Enumeración inicial

Reconocimiento de red con Nmap

Para comenzar la fase de reconocimiento, ejecutamos un escaneo rápido con Nmap sobre la dirección IP de la máquina objetivo:

nmap -F -sV -n -Pn 10.129.73.31

Escaneo de Reactor con nmap

El resultado del escaneo revela dos servicios activos. Entre ellos, el principal vector de interés es el servicio expuesto en el puerto 3000.

Inspección del servicio web

Navegamos a través del explorador hacia la dirección http://10.129.66.100:3000/, donde nos recibe la interfaz del sistema de monitoreo REACTORWATCH CORE MONITORING SYSTEM v3.2.1.

Interfaz de ReactorWatch Core Monitoring System

Al investigar los detalles del software REACTORWATCH, determinamos que se trata de un laboratorio de pruebas de seguridad desarrollado para simular escenarios vulnerables a React2Shell.


Análisis de la vulnerabilidad React2Shell CVE-2025-55182

El fallo radica en la falta de sanitización al deserializar objetos mediante el protocolo Flight de React Server Components (RSC) en Next.js (App Router).

Al procesar solicitudes dirigidas a Server Actions, el analizador (parser) acepta claves reservadas enviadas por el cliente, permitiendo manipular el estado interno de la memoria e inyectar gadgets de ejecución de código.

React2shell Payload

Payload en GitHub Gist

Esplicación del payload

Thenable Hijacking

React utiliza el patrón Thenable para gestionar la resolución asíncrona de componentes en el servidor. El payload suplanta la estructura de una Promesa mediante tres propiedades:

"then": "$1:__proto__:then": Sobrescribe el método then del objeto deserializado para que el motor de JavaScript lo reconozca como un objeto sustituto de Promesa.

"status": "resolved_model": Setea el estado interno a resuelto. Esto fuerza al parser de React a procesar inmediatamente el contenido del objeto sin esperar una resolución asíncrona real.

"reason": -1: Neutraliza los mecanismos de captura de errores internos de React, evitando que la excepción sea descartada antes de procesar los componentes del payload.

Una vez que el objeto es aceptado como un modelo resuelto, el parser evalúa las propiedades secundarias para reconstruir el estado:

"_formData": {"get": "$1:constructor:constructor"}: Recorre la cadena de prototipos del objeto hasta llegar al constructor del objeto global Function (equivalente a invocar Function()).

"_response": { "_prefix": "..." }: Define el contenido del argumento que se pasará a Function(). La cadena almacenada en _prefix es evaluada como código JavaScript arbitrario en el ámbito global del servidor.

Nota. Function() es equivalente a un eval() , pero con la ventaja de que se ejecuta siempre en el ámbito global, lo que garantiza el acceso process.

Escape del Ámbito y RCE

Como las funciones de Server Components se ejecutan en un contexto restringido donde require() no siempre está expuesto directamente en el ámbito local, el payload recurre a la navegación del proceso de Node.js:

El payload accede al objeto global process y navega a través de process.mainModule.require para cargar dinámicamente el módulo nativo child_process.

Acto seguido, ejecuta de forma síncrona el comando de la shell del sistema operativo mediante execSync(), almacenando la salida capturada directamente en la variable interna res.

Exfiltración mediante “NEXT_REDIRECT”

Las aplicaciones Next.js no suelen devolver la salida de errores no controlados en el cuerpo HTTP. Para exfiltrar el resultado del comando, se manipula el sistema de excepciones del framework:

Para exfiltrar la salida de la ejecución, el payload instancia un objeto de tipo Error y le asigna la propiedad digest formateada con la estructura interna que espera Next.js (NEXT_REDIRECT;307;URL...),inyectando el resultado del comando directamente en el campo de la URL.

Al procesar esta excepción manipulada, el framework asume por error que se ha invocado una función redirect() legítima, interrumpiendo la ejecución normal y emitiendo una respuesta HTTP 307 Temporary Redirect que devuelve la salida del sistema operativo dentro de la cabecera HTTP Location.

Por último, aunque en redirecciones web convencionales Next.js puede emplear códigos 307, al procesar un error de tipo NEXT_REDIRECT provocado dentro de una Server Action (POST), el framework responde con 303 See Other.

El código 303 indica al cliente que la acción en el servidor se completó y que la redirección debe consumirse mediante GET para evitar el reenvío de datos. Además, Next.js sustituye la cabecera estándar Location por x-action-redirect, permitiendo que su router cliente intercepte la respuesta y procese la navegación sin recargar la página.

React2shell Payload Response

Tras la instrucción enviada en la solicitud, procedemos a revisar la respuesta devuelta por el servidor. La salida del comando ejecutado se refleja directamente en la cabecera HTTP x-action-redirect, así como en la estructura JSON de la respuesta bajo la clave __page__.

Con esto confirmamos la ejecución correcta de comandos en el sistema objetivo, identificando que el proceso del servicio web se ejecuta con los privilegios del usuario node.

Enumeración del sistema

Tras obtener acceso inicial, procedemos a realizar una inspección del entorno local con el fin de identificar usuarios relevantes, archivos de configuración o credenciales expuestas en la máquina.

Identificación de usuarios

Comenzamos examinando el archivo /etc/passwd para listar los usuarios que disponen de acceso interactivo en el sistema:

root
engineer

Inspección de la base de datos (reactor.db)

Entre los archivos listados con ls vemos el archivo reactor.db que podemos consultar con sqlite3 reactor.db \".dump\" y entre todos datos encontramos la siguiente cadena, un MD5 de lo que puede ser la contraseña del usuario engineer

Durante la revisión del directorio de la aplicación con el comando ls, localizamos el archivo reactor.db. Procedemos a volcar el esquema y su contenido mediante sqlite3:

sqlite3 reactor.db ".dump"

Extracción de datos en reactor.db

Entre la información devuelta por la base de datos se observa la creación de la tabla users junto con los registros de cuentas existentes:

CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    username TEXT NOT NULL,
    password_hash TEXT NOT NULL,
    role TEXT NOT NULL,
    email TEXT
);

INSERT INTO users VALUES(1,'admin','a203b22191d744a4e70ada5c101b17b8','administrator','admin@reactor.htb');
INSERT INTO users VALUES(2,'engineer','39d97110eafe2a9a68639812cd271e8e','operator','engineer@reactor.htb');

En la tabla destaca el hash asociado al usuario engineer: 39d97110eafe2a9a68639812cd271e8e.

Identificación del algoritmo de hash

Para verificar el tipo de cifrado utilizado antes de intentar su ruptura, pasamos la cadena por la herramienta hash-identifier.

hash-identifier 39d97110eafe2a9a68639812cd271e8e
Possible Hashs:
[+] MD5
[+] Domain Cached Credentials - MD4(MD4(($pass)).(strtolower($username)))

Cracking de credenciales con Hashcat

Procedemos a guardar la cadena del hash en un archivo e iniciamos el ataque de fuerza bruta por diccionario utilizando Hashcat y la lista de palabras rockyou.txt:

echo -n "39d97110eafe2a9a68639812cd271e8e" > engineer.hash
hashcat -m 0 engineer.hash /usr/share/wordlists/rockyou.txt

El parámetro -m 0) especifica a Hashcat que la función de resumen utilizada es MD5.

Hash de engineer roto con Hashcat

Acceso vía SSH

Con las credenciales obtenidas, nos conectamos a la máquina objetivo mediante SSH autenticándonos como el usuario engineer.

ssh engineer@10.129.73.31

Una vez dentro del sistema, inspeccionamos el directorio personal del usuario y leemos el archivo user.txt para obtener la primera flag.

Obtención de la flag de usuario

Escalada de privilegios

Enumeración de servicios internos

Una vez establecido el acceso como el usuario engineer, procedemos a inspeccionar los puertos que se encuentran escuchando exclusivamente en el ámbito de localhost:

ss -tulpn

Inspeccion de los puertos

Durante esta revisión, identificamos un servicio activo localmente en el puerto 9229.

Análisis del servicio Node.js Inspector

El puerto 9229 corresponde a la interfaz predeterminada del protocolo Node.js Inspector (el puerto 9230 suele emplearse cuando coexisten múltiples instancias). Debido a que está vinculado únicamente a la interfaz de loopback (127.0.0.1), este puerto no fue detectado en el escaneo externo inicial de Nmap.

La interfaz de depuración de Node.js expone un entorno REPL (Read-Eval-Print Loop) interactivo que permite evaluar y ejecutar código JavaScript de manera directa dentro del contexto del proceso en ejecución.

En consecuencia, si el proceso expuesto se ejecuta en el sistema con permisos administrativos, esta interfaz se convierte en un vector directo para la elevación de privilegios.

Identificación del propietario del proceso

Para confirmar si el proceso en depuración se ejecuta con privilegios elevados, inspeccionamos la lista de procesos filtrando por el ejecutable de Node:

ps aux | grep node

Identificación del proceso Node.js

Explotación de Node.js Inspector

Confirmado que el servicio de depuración pertenece a un proceso ejecutado como root, procedemos a conectarnos a la interfaz mediante el cliente de depuración integrado en el propio ejecutable de Node:

node inspect 127.0.0.1:9229

Al establecer la conexión, ingresamos a un entorno interactivo REPL que evalúa código JavaScript con los mismos privilegios del proceso inspeccionado.

Dentro de la consola de depuración, aprovechamos el módulo nativo child_process mediante process.mainModule.require para ejecutar comandos

Primero, verificamos la identidad del usuario:

exec('process.mainModule.require("child_process").execSync("id").toString()')

Lectura de laRoot flag

Tras validar que la respuesta confirma la ejecución como el usuario root, procedemos a leer directamente el contenido de la flag final ubicada en /root/root.txt

exec('process.mainModule.require("child_process").execSync("cat /root/root.txt").toString()')

Lectura de laRoot flag