教程和靶場來源于 Burpsuite 的官網 Portswigger:File upload vulnerabilities - PortSwigger
原理與危害
很多網站都有文件上傳的功能,比如在個人信息頁面允許用戶上傳圖片作為頭像。如果網站應用程序對用戶上傳的文件沒有針對文件名、文件類型、文件內容或文件大小做充分的驗證,就可能導致允許攻擊者上傳有潛在危險的文件,比如服務端的腳本文件,然后訪問該文件以觸發代碼執行。
文件上傳漏洞的危害取決于兩個因素:
- 網站應用程序對文件名、文件類型、文件內容或文件大小等參數中的哪一個沒有做充分的驗證。
- 上傳文件成功后受到哪些限制。
最壞的情況是文件類型沒有受到服務器的任何限制,允許 .php 或 .jsp 作為腳本文件執行,那么攻擊者可以上傳 .php 或 .jsp 文件作為 webshell,接管服務器。
其他的一些危害:
- 對文件名沒有做驗證,攻擊者上傳一個同名的文件,覆蓋服務器上已存在的文件,如果還存在路徑遍歷漏洞,那么危害更大,可以覆蓋服務器的系統文件。
- 對文件大小沒有做驗證,攻擊者上傳一個非常大的文件,占用服務器的磁盤空間,也就是?DoS 攻擊。
由于文件上傳漏洞的危害很大,所以現在大部分網站應用程序都做了防御,能直接上傳 .php 或 .jsp 文件的網站很少看到了。然而,漏洞仍然會產生,這是因為開發人員堅信他們的防御足夠有效。
靜態文件處理
在剛學習 Web 安全時,看了很多課程和書籍,我們潛意識認為一個 URL 對應網站服務器上的一個文件,對應關系是 1:1,比如 http://example.com/includes/func.php 這個 URL 在網站服務器上對應的文件位于網站根目錄下的 includes 目錄下的 func.php 文件。這種理解在以前靜態網站甚至 PHP 網站和 ASP 網站流行的時候確實是這樣,這只不過是?Apache 或 IIS 等中間件正好這樣管理站點的文件,但是現代 Web 應用程序已經不是這樣 URL 與文件呈現一對一的映射關系,我們現在看到一個 URL 比如? https://csdn.net/mp_blog/creation/editor,在網站服務器并不存在一個路徑為 <網站根目錄>/mp_blog/creation/editor 的文件。
當然,像圖片、CSS 和 JS 等靜態資源,Web 服務器仍然用一對一的映射關系處理它們,處理步驟是:解析 URL 中的 path 部分,識別出請求的文件的擴展名,然后根據擴展名匹配對應的?MIME,最后根據服務器預先的配置執行下一步:
- 如果文件類型是不可執行的,就作為靜態文件把文件內容響應給客戶端。
- 如果文件類型是可執行的,例如 PHP 文件,就創建運行環境,根據請求頭和請求參數分配變量,然后執行腳本。
- 如果文件類型是可執行的,但服務器被配置成不執行該類型的文件,就給客戶端響應一個錯誤。不過,大多數情況下,服務器仍然把這些當成普通文本,將它們的內容返回給客戶端,這一點特性可能被利用實現泄露代碼或其他敏感信息的目的。
實驗
實驗說明:
服務器沒有對上傳的文件沒做任何驗證,上傳一個 PHP 腳本并讀取 /home/carlos/secret 文件的內容就能完成實驗。
進入實驗場景:
用賬號 wiener:peter 登錄,進入個人信息頁面:
點擊最下面的“瀏覽”按鈕,選擇一個 PHP 腳本上傳,PHP 腳本的內容:
<?php echo file_get_contents("/home/carlos/secret"); ?>
讀取并返回 /home/carlos/secret 文件的內容。
點擊“Upload”按鈕上傳文件,響應結果:
結果表示文件上傳成功并給出保存的文件路徑。除了這里可以看到保存的文件路徑,還可以返回個人信息頁面打開 F12 查看路徑:
訪問?/files/avatars/info.php 就能讀取到 /home/carlos/secret 文件的內容:
復制這段數據,返回到首頁點擊 “Submit solution” 按鈕提交即可。
繞過驗證機制
Content-Type 偽造
提交表單后客戶端發送一個 POST HTTP 請求,Content-Type 是請求包中的一個請求頭,表示請求體的內容類型,如果是發送一段簡短的文本,Content-Type 的值一般是?application/x-www-form-url-encoded,但如果發送的是一個大文件,比如 PDF,那么就得用文件上傳的方式發送,此時的 Content-Type 是?multipart/form-data,表示表單數據多個部分,而 HTTP 請求包內容類似于這樣:
POST /images HTTP/1.1
Host: normal-website.com
Content-Length: 12345
Content-Type: multipart/form-data; boundary=---------------------------012345678901234567890123456---------------------------012345678901234567890123456
Content-Disposition: form-data; name="image"; filename="example.jpg"
Content-Type: image/jpeg[...binary content of example.jpg...]---------------------------012345678901234567890123456
Content-Disposition: form-data; name="description"This is an interesting description of my image.---------------------------012345678901234567890123456
Content-Disposition: form-data; name="username"wiener
---------------------------012345678901234567890123456--
表單中每個輸入之間用一段 ---------------------------012345678901234567890123456?字符串分割,第一個輸入是圖片,它也有自己的 Content-Type,值是 image/jpeg,表示這部分屬于圖片類型。第二個輸入是沒有 Content-Type,其實它相當于一個請求參數,參數名是 decsription,參數值是?
This is an interesting description of my image.
第三個輸入同上。
如果服務器在接受上傳的文件時,根據 Content-Type 決定文件的類型,那么就能讓攻擊者利用偽造繞過。如圖:
結合路徑遍歷漏洞
上傳文件的存儲目錄可能被限制為不允許執行腳本,這的確是可以實現的配置。例如,Apache 服務器可以針對某個特定目錄配置特性,只要在目錄下放置一個 .htaccess 文件,里面配置如下的指令:
php_flag engine offphp_flag engine off
那么當前目錄下的所有腳本被訪問時都不會運行,而是當作普通文本返回。
嘗試利用路徑遍歷漏洞繞過這個限制,把文件上傳到存儲目錄的上一級目錄:
訪問該文件時注意它的路徑,avatars/../info.php 其實就是 info.php。
這里用 URL 編碼處理了斜杠,因為應用程序在保存上傳之前對文件名做了一次 URL 解碼處理:
<?php
$target_dir = "avatars/";
$target_file = urldecode($target_dir.$_FILES["avatar"]["name"]);if (strpos($target_file, ".htaccess")) {echo "The upload of .htaccess files is prohibited.";http_response_code(403);
} else if (move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file)) {echo "The file ". htmlspecialchars( $target_file). " has been uploaded.";
} else {echo "Sorry, there was an error uploading your file.";http_response_code(403);
}
?>
如果沒有做 URL 解碼處理,文件名中的 ../ 或 ..\ 會被去掉。我在本地做了一個測試,如圖:
也就是說文件上傳漏洞結合路徑遍歷漏洞需要滿足一定的條件。
重寫服務器配置
有些應用程序對文件上傳功能的安全防護是設置一個后綴名黑名單,里面包含不允許上傳的文件后綴名,但這仍然存在風險,因為無法囊括所有不合法的后綴名,比如 .php5、.shtml 等等,這些可能會被忽略。另外,相關配置文件的文件名或后綴名也應該被列為黑名單。
大多數 Web 服務器并不是在一開始安裝成功后就能執行文件的,像 Apache,要執行 PHP 腳本,需要在 /etc/apache2/apache2.conf 配置文件中添加如下指令:
LoadModule php_module /usr/lib/apache2/modules/libphp.soAddType application/x-httpd-php .php
意思是加載 php 模塊,以及當訪問后綴名為 .php 的文件時,作為 php 腳本執行。
不止 Apache,Nginx 和 IIS 也都是要做一些配置才能執行腳本。
除了 apache2.conf 這個系統級的配置文件,Apache 服務器允許在每個目錄下放置一個 .htaccess 文件,在該文件中添加的指令可以覆蓋和補充 apache2.conf 的配置。如果應用程序允許上傳 .htaccess,那么就能利用它繞過安全防護,執行代碼。
首先,第一步是先上傳 .htaccess:
文件內容:
AddType application/x-httpd-php .png
這表示 .png 后綴的文件也被當成 PHP 腳本執行。
第二步是上傳包含 PHP 代碼的 .png 文件:
最后一步是訪問 phpinfo.png 文件,觸發代碼執行。
IIS 服務器也有一個類似的配置文件,名為 web.config,它的指令示例:
<staticContent><mimeMap fileExtension=".json" mimeType="application/json" /></staticContent>
表示 .json 文件被當成 json 發送給客戶端。
PS:通常,.htaccess 和 web.config 被從客戶端禁止訪問。
混淆文件后綴
下面列舉一些混淆技巧:
- 如果在驗證文件后綴名是否合法時區分大小寫,但是在后綴名映射?MIME 時不區分大小寫,那么可嘗試大寫后綴,比如 exploit.pHp、exploit.PHP 等。
- 多后綴名,比如 exploit.php.jpg。(Apache 多后綴解析漏洞)
- 末尾加點,比如 exploit.php.? 。(Windows不允許文件名末尾有點,如果有,會自動去掉)
- 在驗證文件后綴名之前沒有做 URL 解碼處理,但是在之后做了,那么可用 URL 編碼繞過安全驗證,比如?exploit%2Ephp。
- 如果服務器用低級語言如 C/C++ 寫的,那么可嘗試空字節繞過,比如?exploit.asp%00.jpg。
- 結合 IIS 6.0 解析漏洞,如:exploit.asp;.jpg。
- 如果應用程序匹配到不合法的后綴名后只做一次刪除處理,而不是遞歸刪除,那么可雙寫后綴繞過,比如 exploit.p.phphp。
下圖是利用空字節繞過的一個例子:
下面是應用程序處理這個文件名的代碼,我們看看如何處理這個 %00 的:
<?php
$target_dir = "avatars/";
$target_file = urldecode($target_dir . $_FILES["avatar"]["name"]);
$uploadOk = true;$imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));if($imageFileType != "jpg" && $imageFileType != "png") {echo "Sorry, only JPG & PNG files are allowed\n";$uploadOk = false;
}//去掉空字節及其后面的字符
$target_file = strtok($target_file, chr(0));if ($uploadOk && move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file)) {echo "The file ". htmlspecialchars( $target_file). " has been uploaded.";
} else {echo "Sorry, there was an error uploading your file.";http_response_code(403);
}
這很容易看懂。然而,在實際的場景中,并不會有開發人員會這樣處理文件名,讓攻擊者有利用空字節繞過的可能,更多是服務器由 C/C++ 開發時才有可能出現。
文件內容驗證繞過
當應用程序不信任 Content-Type 所指示的那樣判斷一個文件的類型,可能會直接通過驗證文件的內容來判斷。對于這種情況,最簡單的驗證方式就是查看文件開頭幾個字節,這幾個字節又稱魔術數字,大多數類型的文件都會在開頭設置獨一無二的字節序列,比如 JPEG 文件的就是?FF D8 FF,這種驗證方式很容易繞過,因為攻擊者也能在腳本的開頭放置這幾個字節并且不會影響代碼執行。
比較復雜的情況是,應用程序用一些圖片處理函數來判斷上傳的文件的數據是否真的是合法的圖片數據,比如獲取圖片的尺寸,腳本文件就不會有這種數據。但是,攻擊者可以將代碼嵌入到圖片的一些數據區,這些數據區不會破壞圖片本身的數據和圖片的渲染,比如 JPEG 圖片的注釋部分,用 Exiftool 工具可以做到這些事情。(Exiftool 工具在 kali 系統中已默認安裝)
Exiftool 將 PHP 代碼嵌入到 JPG 圖片的注釋部分:
exiftool -Comment="<?php echo 'START ' . file_get_contents('/home/carlos/secret') . ' END'; ?>" test.jpg -o polyglot.php
意思是將 -Comment 參數指定的內容嵌入到 test.jpg 的注釋部分,最后生成 polyglot.php 文件。
條件競爭上傳文件
有些應用程序接收到上傳的文件后先保存到網站的一個目錄下,接著對文件的安全性做驗證,如果文件被判斷為不安全的就會刪除該文件,這樣的處理是有問題。在保存到目錄后和被判斷為不安全而刪除之前存在一個時間空隙,攻擊者在這段空隙訪問所上傳的文件就能執行該文件,這是能夠做到,利用預先寫好的程序快速不停地訪問即可。
現代開發框架都內置了處理文件上傳的組件,它們通常都能夠阻止這類漏洞,其處理步驟是
- 把上傳的文件保存到一個沙盒化的臨時目錄(這里的文件被隔離且無法被訪問到并執行);
- 重命名為一個隨機字符串以避免文件覆蓋;
- 做安全性驗證,如果文件被判斷為安全的,就移動文件到最終保存的目錄,否則就刪除。
如果開發人員自己編寫文件上傳處理程序,那么就有可能存在這類漏洞。
攻擊者很難在黑盒測試中找到這種漏洞,除非他能找到應用程序的源代碼,通過審計發現這種漏洞。
另外,還有一種情況可能發生條件競爭文件上傳漏洞,就是讓用戶提供一個 URL 來保存文件,這一般只能由開發人員自己編寫處理程序,也必須先下載一份文件副本到本地,然后再做安全性驗證,所以這種情況很容易出現漏洞。
如果上傳的文件或通過 URL 下載的文件保存到一個具有隨機名稱的臨時目錄,這一般不太可能被攻擊者知道,但如果臨時目錄是偽隨機數,那么仍然有可能被攻擊者用暴力破解的方法知道。
為了讓條件競爭上傳文件的攻擊更容易,攻擊者會上傳一個惡意的大文件,里面填充大量無用字符,但不會影響到里面的代碼被執行,應用程序處理這類大文件會處理得更久,那么文件在文件系統上停留的時間更久,也就更容易被攻擊者趁機訪問。
客戶端攻擊
能上傳帶有惡意代碼的文件是最嚴重的漏洞,這是針對服務器的攻擊,但文件上傳也能用于針對客戶端攻擊。上傳一個 HTML 文件或 SVG 圖像,里面包含 XSS 代碼,那么就相當于是一個存儲型 XSS 漏洞了,然后把文件的 URL 發給受害者,因為域名是合法的,所以更容易被信任。(注意,上傳的文件如果被保存到另一個網站,那么要考慮同源策略的影響)
PUT 文件上傳
某些服務器被配置為允許 HTTP PUT 請求方法,這種請求方法是用于上傳文件,攻擊者可先用 OPTIONS 請求方法查看網站是否支持 HTTP PUT 請求方法,然后再嘗試上傳文件。
PUT 上傳請求示例:
PUT /images/exploit.php HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-httpd-php
Content-Length: 49<?php echo file_get_contents('/path/to/file'); ?>
防護
下面是一些防護方法:
- 使用后綴名白名單而不是黑名單,因為列舉出所有允許的后綴名更容易。
- 文件名不能包含 ../ 序列。
- 重命名文件避免文件覆蓋。
- 對文件做完安全性驗證再移動它到永久保存的目錄,在此之前保存到臨時目錄。
- 盡量使用框架內置的文件上傳處理組件,而不是自己編寫。