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.