Reactor – HackTheBox Writeup#
Difficulty: Easy | OS: Linux | Season: 11
TL;DR#
Reactor chains a modern web exploit with classic operational mistakes. A vulnerable Next.js Server Actions implementation gives unauthenticated code execution as node, sensitive credentials are exposed in a local SQLite database, and a root-owned Node.js debug interface (--inspect) finishes the box.
Quick Stats#
| Stage | Detail |
|---|---|
| Foothold | CVE-2025-55182 (react2shell) on Next.js 15.0.3 |
| User Pivot | Cracked MD5 hash → engineer:reactor1 |
| PrivEsc | Root Node.js inspector abuse on 127.0.0.1:9229 |
| Impact | Full root compromise |
Overview#
Reactor is a Linux machine centred around a vulnerable Next.js 15 web application. The attack chain is:
- Reconnaissance — discovering a Next.js app on port 3000
- Initial foothold — unauthenticated RCE via CVE-2025-55182 (react2shell)
- Credential harvesting — extracting and cracking MD5 hashes from a SQLite database
- Lateral movement — SSH access as
engineer - Privilege escalation — abusing an exposed Node.js inspector interface running as root
1. Reconnaissance#
Port scan#
| |
| |
Follow up with a detailed nmap scan to confirm service versions:
| |
| |
Findings:
- Port 22 — OpenSSH (standard lateral movement target once creds are found)
- Port 3000 — Next.js web application confirmed by
X-Powered-By: Next.jsheader
Web application fingerprinting#
Visiting http://10.129.2.30:3000 presents the ReactorWatch Core Monitoring System — a nuclear sensor monitoring dashboard built by Nuclear Dynamics Corp.
We already know it’s Next.js from the nmap output, but nmap doesn’t give us the version number. We need to dig deeper to pin the exact release.
Method 1 — Inspect the full HTTP response headers#
| |
| |
X-Powered-By: Next.js confirms the framework but still no version. Move to the page source.
Method 2 — Check the HTML source for the version meta tag#
| |
| |
The __NEXT_DATA__ JSON block is injected by every Next.js app. Older Next.js versions leak the full version in a <meta> tag; newer ones (like the App Router era) don’t — so move to the static asset paths.
Method 3 — Version from static chunk paths (most reliable)#
Next.js serves its runtime JavaScript from /_next/static/chunks/. The chunk filenames and structure changed significantly between major versions — we can pull them to find version strings baked directly into the bundle.
| |
| |
Fetch the main framework chunk and grep for the version:
| |
| |
Next.js 15.0.3 confirmed.
Method 4 — Confirm via package.json (after foothold)#
Once we have RCE (or a shell), we can read the app’s package.json directly:
| |
| |
This is the definitive confirmation and is also useful for identifying other vulnerable dependencies.
Summary of fingerprinting: Nmap told us Next.js. The static chunk grep told us 15.0.3. That specific version maps directly to CVE-2025-55182 — the whole attack path follows from this one version number.
Note: Always check
X-Powered-By, page source meta tags, and/_next/staticchunk content for version strings before reaching for any scanner tool. These passive reads leave minimal noise in access logs.
2. Initial Foothold — CVE-2025-55182 (react2shell)#
Vulnerability background#
CVE-2025-55182, known as react2shell, affects Next.js applications using Server Actions. The vulnerability abuses:
- The
Next-ActionHTTP header, which triggers server-side action handling - Prototype pollution via a crafted multipart request body
- Unsafe deserialisation that allows access to
process.mainModule.require
This results in unauthenticated remote code execution as the user running the Next.js process.
In short: if an application accepts a malicious Next-Action request and unsafely deserialises crafted multipart data, you can execute OS commands without any authentication.
The exploit#
Save the following as react2shell.py:
| |
Confirm RCE#
| |
| |
RCE confirmed as uid=999(node). The command output is returned inside the digest field of the error response.
Get a reverse shell#
Start a listener on your Kali machine:
| |
Option A — Plain reverse shell (may fail in some environments)#
Some targets or WAFs will reject raw special characters like >& and 0>&1 in query strings or multipart fields. Try this first:
| |
Option B — Base64-encoded payload (recommended if Option A fails)#
If Option A gives an error or the shell never connects, the special characters are likely being stripped or escaped somewhere in the request. Encoding the payload side-steps that entirely.
Step 1 — Generate the base64 payload on your machine:
| |
| |
Tip: Use
-nto suppress the trailing newline — a stray\nin the base64 string will break decoding on the target.
Step 2 — Send the encoded payload via the exploit:
| |
${IFS} is used in place of spaces because spaces inside the exploit’s execSync call can cause the shell to mis-parse the command.
Alternatively, using brace expansion (no ${IFS} needed):
| |
Shell received on your listener:
| |
The ioctl and job control warnings are normal for a non-tty shell — ignore them.
Stabilise the shell#
Upgrade to a proper interactive TTY before enumerating:
| |
Then background the shell with Ctrl+Z, fix your terminal, and bring it back:
| |
This gives you proper tab completion, arrow keys, and Ctrl+C without killing your session.
3. Credential Harvesting#
Explore the filesystem#
| |
| |
Two directories. Start with the main app — it almost always has config files with credentials.
Read the environment file#
| |
| |
The .env reveals a SQLite database at /opt/reactor-app/reactor.db. Local app databases are a gold mine — they frequently hold operational credentials reused for SSH or other services.
Extract credentials from the database#
Quick grep to find the interesting tables:
| |
| |
Full dump via sqlite3:
| |
| |
Both hashes are MD5 (32-character hex, no salt prefix). MD5 is fast and unsalted, so offline cracking with rockyou is very practical.
Crack the hashes#
| |
Results:
a203b22191d744a4e70ada5c101b17b8→admin(application password, not needed for SSH)39d97110eafe2a9a68639812cd271e8e→reactor1(engineer’s password — this is the pivot)
4. User Flag — SSH as engineer#
| |
| |
| |
| |
5. Privilege Escalation — Node.js Inspector Abuse#
Identify the vulnerable process#
| |
| |
Why this is critical:
- The Node.js
--inspectflag enables the Chrome DevTools Protocol — it is a full JavaScript REPL running as root - Bound to
127.0.0.1:9229— so it’s only reachable from localhost - Because we have SSH as
engineer, we can port-forward 9229 and interact with it directly - No authentication is required by default on the debug port
This is a classic “localhost means safe” misunderstanding. Once you have any shell on the machine, local-only debug services are fully reachable.
Examine the monitored script#
| |
| |
A simple uptime checker — root is running it with a full debugger attached for no reason.
Set up SSH port forwarding#
From your Kali machine, tunnel the remote debug port to localhost:
| |
Verify the tunnel is active:
| |
| |
Get the WebSocket debugger URL#
| |
| |
Copy the webSocketDebuggerUrl — it changes every time the process restarts.
Connect and execute as root#
Install wscat if needed:
| |
Start a root shell listener:
| |
Connect to the inspector:
| |
| |
Paste the payload at the > prompt:
| |
Important: Use
process.mainModule.require— not barerequire. The inspector context doesn’t have arequirebinding in scope, so using it directly throwsReferenceError: require is not defined.
Inspector confirms execution:
| |
Root shell received:
| |
If nc was sent to background, bring it forward with fg.
Root flag#
| |
| |
Attack Chain Summary#
| |
Key Lessons#
Initial access: Next.js Server Actions can be dangerous when deserialisation of multipart request bodies is mishandled. The Next-Action header should only be honoured for legitimate, signed action IDs — framework version pinning and WAF rules targeting multipart abuse are essential mitigations.
Reverse shells: Special characters (>&, 0>&1) are frequently stripped or escaped by the vulnerable application’s input handling, WAFs, or shell parsers. Base64 encoding your payload is a reliable workaround that avoids character-level filtering entirely and should be a default habit.
Credential exposure: SQLite databases embedded in web applications frequently contain user credentials. MD5 is not a safe password hashing algorithm — use bcrypt, scrypt, or Argon2id with a unique per-user salt instead.
Privilege escalation: The Node.js --inspect flag must never be used in production. Even when bound to localhost, it becomes a full root RCE primitive the moment any low-privileged shell exists on the machine. Debug interfaces provide unrestricted JavaScript execution in the context of the running process.
Defender Fixes#
- Upgrade Next.js to a patched release and disable unsafe Server Action deserialization patterns.
- Replace MD5 with Argon2id or bcrypt and rotate all existing credentials immediately.
- Move secrets out of readable app paths (
.env, local DB files); enforce least-privilege file permissions (e.g.chmod 600 .env, owned by the app user only). - Never expose
--inspectin production. If debugging is unavoidable, isolate it behind strong authentication and network-level controls. - Audit all root-owned long-running processes for debug flags, writable config paths, or unnecessary capabilities.
Final Thoughts#
Reactor is labeled easy, but it reflects real-world failure patterns: modern framework bugs plus weak credential hygiene plus insecure debug tooling. Chaining “small” misconfigurations — each individually survivable — is often all an attacker needs to move from zero to root. The box is a good reminder that security in depth means hardening every layer, not relying on network isolation alone.




Comments & Discussion
Share your thoughts, analysis, or questions below