昨天整理了碰到 Web 服務時的完整測試流程——先做 Information Gathering,看看網站本身、原始碼,以及 robots.txt 有沒有可以直接利用的資訊。
真的沒找到突破口,再開始跑目錄爆破,找看看有沒有其他隱藏的目錄或檔案。
找到新的目錄或頁面後,再對它做一次 Information Gathering,繼續看看有沒有新的線索。
如果前面的資訊還不足以達到我們的目的,而網站上又有輸入點,就可以進一步針對輸入點做測試,依照輸入點的類型選擇適合的攻擊測試。

我們在Day 12已經一起看過 SQL Injection,今天就來看看Web的另一個常見攻擊Command Injection。
它跟 SQL Injection 的成因邏輯其實很像,只是這次的目標不是資料庫,而是作業系統。
也因為這樣,如果成功,影響的不只是資料庫裡的資料,還可能進一步影響到伺服器上的系統環境。
我們趕快一起來看看!
有些網站需要直接呼叫系統指令來完成某些功能,例如查庫存、產生報表、發送通知。
這時候,後端可能會把使用者傳進來的資料,直接帶進系統指令裡一起執行。
正常情況下,我們傳的是「商店編號」或「檔案名稱」這類資料。
但如果網站沒有做好輸入檢查,我們就可能在原本的輸入裡加入其他指令,讓伺服器一起執行。這就是 Command Injection。
不一定要有輸入框才算輸入點。按鈕、下拉選單,甚至是 URL 或 Request 裡的參數,只要資料會被送到伺服器,就有可能成為我們進一步測試的地方。
按鈕按下去,背後就有一個 HTTP 請求送出去,裡面帶著我們傳入的參數——只是前端沒有讓我們直接改。Burp 可以攔截這個請求,讓我們在送出去之前修改裡面的值。
今天的 Lab 就是這種情況——購物網站的商品頁面有個 Check stock 功能,選一個商品查庫存。看起來只是一個普通的按鈕,但按下去會把參數傳到伺服器去處理,這就是輸入點。
看到輸入點,先試 SQL Injection——為什麼?
「查庫存」這種功能,後端很可能會查詢資料庫,所以 SQL Injection 可以作為其中一個測試方向。像是先加入單引號,觀察有沒有 SQL 錯誤或其他異常反應。
如果 SQL Injection 沒有明顯反應,再換個方向想:
後端不一定是查資料庫——也可能是執行系統指令去查庫存。我們從外面看不到它怎麼做的 >< 如果後端是用系統指令的話,就可能有 Command Injection 的機會,所以SQL Injection、Command Injection都要試。
那要如何測試 Command Injection?我們可以在原本傳給伺服器的值後面「加料」——塞進我們想要執行的指令><
;——不管前面成不成功,都繼續跑
check_stock 1; whoami
不管查庫存有沒有成功,whoami 都會執行。最常用,最直接。先試這個。
&&——前面成功才跑下一個
check_stock 1 && whoami
查庫存成功了,才繼續跑 whoami。如果查庫存失敗,whoami 就不跑了。
||——前面失敗才跑下一個
check_stock 1 || whoami
前面的指令成功,whoami 就不會執行;只有前面的指令失敗,才會繼續執行 whoami。這種做法輸出最一目了然。
|——把前面的輸出傳給後面
check_stock 1 | whoami
把查庫存的結果傳給 whoami 當輸入。
先試 ; 就好——不管前面的指令成功或失敗,後面的指令都會繼續執行。
假設這個功能的後端是用系統指令做的:
storeId = request.GET['storeId']
os.system("check_stock " + storeId)
我們選商店 1,系統就跑:
check_stock 1
正常,回傳庫存數量。
但如果我們把 storeId 改成 1; whoami,系統就跑:
check_stock 1; whoami
拆開來看:
check_stock 1 ← 查了庫存
; whoami ← 然後跑了 whoami!
; 在 Shell 裡是「跑完這個,再跑下一個」的意思——whoami 真的在伺服器上執行了,伺服器的使用者名稱就這樣跑出來了。
根本問題:跟 SQL Injection 一模一樣——網站沒有分辨我們傳的是「正常的輸入值」還是「系統指令」,直接把輸入拼進作業系統執行。
| SQL Injection | Command Injection | |
|---|---|---|
| 成因 | 輸入被拼進 SQL 查詢 | 輸入被拼進系統指令 |
| 主要影響 | 資料庫中的資料 | 作業系統層級的指令執行 |
| 實際影響 | 取決於資料庫權限與設定 | 取決於 Web 服務帳號的權限與環境 |
| 常見防禦 | Prepared Statement | 輸入驗證、避免直接呼叫系統指令 |
兩個漏洞實際能造成多大的影響,還是要看當下執行的帳號有哪些權限。。
記得 Bashed 嗎?
當時 Web 服務跑在 www-data,所以就算透過 Web 執行指令,也還是只有 www-data 的權限,很多需要 root 權限才能做的事情還是沒辦法執行。
Lab:OS command injection, simple case
目標:注入whoami,讓頁面顯示目前的使用者。
點 ACCESS THE LAB,進入今天練習的LAB
跟 Day 12 一樣,我們用 Burp Repeater 來測試:
我們把 FoxyProxy 圖示切換到 Burp 模式
(如果還沒安裝FoxyProxy、執行相應設定,可以先參考Day 12 — SQL Injection:一個單引號,就能撬開整個資料庫的先設定 Burp Suite 攔截請求區塊的內容)
-
Burp Suite → Proxy → Intercept is on
進入 Lab 之後,點任何一個商品的 View details。
在 Burp Intercept 裡看到請求,右鍵 → Send to Repeater。

按 Send,看看這個 Request 裡有沒有可以控制的參數,也順便看看 Response 裡有沒有洩漏什麼有用的資訊。
目前這個 Request 看起來沒有什麼明顯可以測試的地方,Response 也沒有看到什麼明顯的資訊洩漏。
回到 Intercept,把 Intercept 關掉。
回到瀏覽器,這時候就可以正常進入 Details 頁面
再回 Burp,把 Intercept 開起來
回瀏覽器頁面按 Check stock。
回 Burp 可以看到我們攔截到這個請求——裡面有 storeId、productId 這類參數。
這兩個參數是用來告訴伺服器「要查哪間店、哪個商品」,而這些值會跟著 Request 傳到伺服器,我們也可以自己修改。
所以雖然畫面上沒有看到輸入框,但這些 Request 裡的參數一樣可以成為我們進一步測試的輸入點。
把這個請求右鍵 → Send to Repeater。
可以在 Repeater 看到這個 Request 的原始內容。
在 Repeater 裡,把參數的值改成:
productId=1&storeId=1;whoami
按 Send。
破關哩!!!🎉
不只產品庫存,我們也成功印出 current user——確認有 Command Injection。
但……還沒結束 ><
雖然這次我們是透過 Command Injection 執行指令,還沒有拿到真正的interactive Shell,但剛好可以趁這個機會,把之前整理的「拿到 Shell 後的 SOP」練習一下!
現在在哪個目錄?
這也是拿到 Shell 後很基本的偵查步驟,之前在 Bashed 還沒有整理進來,今天就一起補上~
這次我們把 Repeater 中的 Body 參數改成:
productId=1&storeId=1;pwd

可以知道我們現在在的目錄。
接著我們換試其他常用的注入符號,觀察每個的反應,也順便搭配之前整理的「拿到 Shell 後的 SOP」,一起溫故知新 xdd
嘗試&&
productId=1&storeId=1&&id

&& 也是常用的注入符號,需要前面的指令成功執行後,才會執行後面的指令。
理論上,如果前面的查詢成功,id 應該會接著執行。
為什麼沒有出現 id 的結果?
因為這個 Lab 的 Request Body 使用 & 來分隔參數。
所以 productId=1&storeId=1&&id 會先被當成 Request 的參數來解析,而不是完整地把 &&id 當成要傳給 Shell 的內容。也就是說,&&id 沒有照我們預期的方式進到系統指令裡。
換成 ; 就成功了:
productId=1&storeId=1;id

用剛剛成功注入的;,確定 id 指令是可以執行的。
嘗試 |
productId=1&storeId=1|sudo -l

出現 sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper
為什麼失敗?sudo -l 在這個執行環境下需要處理密碼/終端機相關的互動,但我們透過 Command Injection 執行指令時,並沒有提供這樣的互動式終端環境,所以這次沒有辦法直接完成。
或許可以透過 Reverse Shell 拿到一個真正的終端機,就像 Bashed 那樣——有了穩定的 shell 之後,很多受限的指令就可以執行了。
嘗試 ||:
productId=1&storeId=1||hostname

如果前面的指令失敗,才會繼續執行 hostname。所以如果我們預期前面的指令可能失敗,就可以用 || 來測試。
嘗試讓 productId=1&storeId=1 失敗:
productId=1&storeId=||hostname

Response 連庫存都不見惹——hostname 可能有執行,但輸出沒有被回傳到頁面。
&& 跟 || 在這個 Lab 的環境都沒有成功——不是每個符號都適用每個環境,遇到失敗就換符號試,不代表沒有 Command Injection。
換成 ; 就成功了:
productId=1&storeId=1;hostname

以剛剛成功注入的符號;確定是可以找到hostname的
我們就用剛剛確認可以成功的 ;,繼續做基本的主機資訊偵查~
productId=1&storeId=1;uname -a

uname -a 可以看到 Kernel 版本、主機名稱、系統架構等資訊。
productId=1&storeId=1;cat /etc/os-release
os-release是檔案,不是指令,所以要用cat讀
今天的偵查做到這裡——從確認「我是誰」到了解「目標機器的作業系統是什麼」
但這只是最基本的><拿到 Shell 之後其實有很多資訊要做偵查。這裡將今天補充的步驟也納入我們之前整理的 Shell 拿到後的 SOP:
今天沒碰到的部分,在前面打 Bashed 的時候有走過一遍,可以參考我們Day 8到Day 10 的Bashed靶機操作記錄
最根本的:不要直接呼叫系統指令
直接呼叫系統指令,就等於把使用者的輸入丟給 Shell 去解析——Shell 遇到 ;、&& 這些符號就會照著執行,這就是問題所在。
很多語言都有內建的方式可以做同樣的事,而且不需要經過 Shell。例如要測試某台機器有沒有在線,Python 可以用 socket 直接建立連線,不用呼叫系統的 ping。這樣使用者的輸入只是純資料,不會被當成指令執行。
一定要呼叫的話:嚴格驗證輸入
如果真的沒辦法避免,就要確認使用者傳進來的值是我們預期的格式——商店 ID 只能是數字、檔名只能有英數字,不符合就拒絕。
而且前端後端都要驗證。前端驗證用 Burp 一秒繞過,後端沒有驗證等於沒有驗證。
把 Web 服務跑在低權限帳號下
就算真的被注入了,低權限帳號能做的事也有限。這也是為什麼 Bashed 拿到 shell 之後還要提權——www-data 不是 root,能做的事受限。
今天的 Command Injection,跟昨天的 SQL Injection,其實有一個很核心的共同點:
網站把使用者的輸入,當成了「程式的一部分」執行,而不是單純當成資料處理。
SQL Injection 的輸入,最後被拼進了 SQL 查詢;
Command Injection 的輸入,則可能被拼進系統指令。
所以根本的問題其實很像,只是「輸入最後被帶到哪裡」不一樣。
今天還有一個重要發現:
沒有輸入框,不代表沒有輸入點。
以前看到一個按鈕,我可能完全不會把它跟 Injection 聯想在一起。
但實際透過 Burp Suite 攔下 HTTP Request 之後才發現,
按鈕背後其實一樣有資料送到伺服器。
只要 Request 裡有我們可以控制的參數,就有可能成為進一步測試的地方。
這次實際操作過之後,才比較理解:
不要只看畫面上有沒有輸入框,而是要去看資料實際怎麼傳。
搞清楚這件事之後,以後看到 Web 服務,腦袋就會慢慢開始出現一些問題:
「這裡有沒有資料會傳到伺服器?」
「這個輸入最後會被拿去做什麼?」
「它有沒有可能被拼進 SQL 查詢裡?」
「有沒有可能被當成系統指令執行?」
我覺得這就是 Web 漏洞測試很基本、但也很重要的思路:
看到輸入 → 追它去了哪裡 → 判斷它最後被怎麼使用。
明天 Day 14,我們來回顧這兩週走過的路~
從 Nmap 開始,一路到 Web 漏洞,看看哪些觀念已經透過這兩週的練習慢慢變成我們的 DNA 🧬哪些還是看到會滿頭問號 xddd
再看看接下來的路,怎麼一起繼續往 CPTS 前進!
明天見啦~