Reactor
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:
- CVE-2025-55182 (React2Shell), an unauthenticated RCE in React Server Components, gives a shell as the
nodeuser. - The app’s SQLite database holds an unsalted MD5 hash for the
engineeraccount, and it cracks in seconds. - A root-owned Node.js process was started with
--inspecton 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 | msf > search cve-2025-55182 |
The module defaults to port 80, so I changed RPORT to 3000 and pointed LHOST at my tun0 address:
1 | msf exploit(...) > set RHOSTS 10.129.6.66 |
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 | ls -la |
reactor.db is readable by the node user, so I dumped the users table:
1 | sqlite3 reactor.db ".tables" |
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 | ┌──(kali㉿kali)-[~] |
A quick check that the hash really matches the password:
1 | ┌──(kali㉿kali)-[~] |
The same password works over SSH, and user.txt is in the home directory:
1 | ┌──(kali㉿kali)-[~] |
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 | node 1395 ... next-server (v15.0.3) |
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 | engineer@reactor:~$ ss -lntp | grep 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 | engineer@reactor:~$ node inspect 127.0.0.1:9229 |
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 | engineer@reactor:~$ ls -la /tmp/rootbash |
The -p flag matters. Without it bash drops the effective UID when it starts, and you end up as engineer again.
Attack Chain
1 | Next.js on :3000 |
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
.envfile 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
--inspecton a privileged process. Binding it to127.0.0.1helps against outside access, but any local user can still attach and run code as the process owner. - Check process owners in every
pslisting. 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. 🙏







