PHP檔案上傳漏洞原理以及防禦姿勢

Hvnt3r發表於2018-06-11

PHP檔案上傳漏洞

本文用到的程式碼地址:https://github.com/Levones/PHP_file_upload_vlun,好評請給小星星,謝謝各位大佬!

0x00 漏洞描述

​ 在實際開發過程中檔案上傳的功能時十分常見的,比如部落格系統使用者需要檔案上傳功能來上傳自己的頭像,寫部落格時需要上傳圖片來豐富自己的文章,購物系統在識圖搜尋時也需要上傳圖片等,檔案上傳功能固然重要,但是如果在實現相應功能時沒有注意安全保護措施,造成的損失可能十分巨大,為了學習和研究檔案上傳功能的安全實現方法,我將在下文分析一些常見的檔案上傳安全措施和一些繞過方法。

​ 我按照最常見的上傳功能–上傳圖片來分析這個漏洞。為了使漏洞的危害性呈現的清晰明瞭,我將漏洞防禦措施劃分為幾個不同的等級來作比較

0x01 前端HTML頁面程式碼

<!DOCTYPE html>
<html>
<meta charset="utf-8">
<title>
    file_upload_test
</title>
<body>
<form enctype="multipart/form-data" action="upload_1.php" method="POST" />
<input type="hidden" name="MAX_FILE_SIZE" value="100000" />
選擇你要上傳的圖片:
<br />
<input name="uploaded" type="file" /><br />
<br />
<input type="submit" name="Upload" value="上傳" />
</form>
</body>
</html>

前端的實現程式碼均為以上。介面如下圖:

Cq0S4e.png

0x01 零防禦的PHP上傳程式碼

原始碼 upload_0.php

<?php
if (isset($_POST['Upload'])) {
    $target_path = "uploads/";
    $target_path = $target_path . basename( $_FILES['uploaded']['name']);
    if(!move_uploaded_file($_FILES['uploaded']['tmp_name'], $target_path)) {
        echo '<pre>';
        echo '您的圖片上傳失敗.';
        echo '</pre>';
    } else {
        echo '<pre>';
        echo $target_path . '檔案已經成功上傳!';
        echo '</pre>';
    }
}
?>

這段PHP程式碼對上傳的檔案沒有任何的過濾,只是將上傳的檔案直接儲存到了網站uploads資料夾下,此時如果我們上傳一個一句話木馬並通過瀏覽器訪問加上引數的地址或者使用中國菜刀直接連線,就可以為所欲為了。

//一句話木馬
<?php eval($_GET['cmd']);?>

0x01 初級防護-驗證檔案型別

原始碼 upload_1.php

<?php
if( isset( $_POST[ 'Upload' ] ) ) {
    $target_path  = "uploads/";
    $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
    //識別檔案型別
    $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
    $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
    $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
    if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&
        ( $uploaded_size < 100000 ) ) {
        if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
            echo "<pre>圖片上傳失敗</pre>";
        }
        else {
            echo "<pre>{$target_path} 圖片上傳成功!</pre>";
        }
    }
    else {
        echo "<pre>只允許上傳jpg或者png格式的圖片檔案,且檔案大小不能超過100k</pre>";
    }
}
?>

防禦方法

初級防禦的程式碼在審查使用者上傳的檔案時加入了“Content-Type”驗證,程式碼會自動識別檔案型別並將檔案型別以表單的形式進行驗證,如果“Content-Type”是image/jpeg或者image/png時檔案可以上傳 成功,算是初級防禦。

繞過方法

用BurpSuite截斷代理修改資料包的相關欄位即可完成繞過,本例上傳的檔案時shell.php,程式碼會將此檔案的Content-Type識別為application/x-php,直接將application/x-php改為mage/jpeg即可繞過驗證,而且對於檔案大小的限制也是可以直接修改”MAX_FILE_SIZE”的方式突破限制從而上傳更大的檔案。

修改前Cq099H.png

修改後

Cq0C3d.png

Cq03D0.png

0x02 一般防護-驗證檔案字尾

原始碼 upoad_2.php

<?php
if( isset( $_POST[ 'Upload' ] ) ) {
    $target_path  = "uploads/";
    $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
    //記錄檔案資訊
    $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
    $uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
    $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
    $uploaded_tmp  = $_FILES[ 'uploaded' ][ 'tmp_name' ];
    //識別檔案字尾
    if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
        ( $uploaded_size < 100000 ) &&
        getimagesize( $uploaded_tmp ) ) {
        if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {
            echo "<pre>圖片上傳識別.</pre>";
        }
        else {
            echo "<pre>{$target_path} 圖片上傳成功!</pre>";
        }
    }
    else {
        echo "<pre>只能上傳格式為jpg和png的圖片.</pre>";
    }
}
?>

相比較於前一種比價簡單的驗證content-type的防護方式,一般級別的防護措施換成了驗證檔案字尾的方式,順便多說一句,在為了安全性設定一些限制時,使用白名單永遠比設定黑名單要安全的多,因為總會有=各種方式繞過黑名單的方式或者是一些針對不同伺服器系統或著伺服器的特殊解析原理而造成的一些安全隱患。以下是獲取檔案字尾的程式碼:

$uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1)

通過本語句獲取檔名中最後一個“.”後的字元識別上傳的檔名的字尾,並將字尾儲存在一個變數中。

if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
        ( $uploaded_size < 100000 ) &&
        getimagesize( $uploaded_tmp ) )

而在if的邏輯判斷中,需要上一條語句擷取到的檔案字尾為“jpg”,“jpeg”或者“png”,切且上傳的檔案大小不得大於10000b,如果只有這個限制方法的話,可以直接使用burpsuite進行00截斷,從而使得在檔案字尾驗證時通過但是在檔案轉儲的時候忽略掉00之後的內容從而實現字尾欺騙,具體方式如下:

  • 假設網站只能上傳圖片檔案並在後臺歐了字尾的限制
  • 此時你要上傳一個shell.php的一句話木馬
  • 將”shell.php”改為”shell.php 1.png”
  • 使用burpsuite截斷代理,攔截資料包
  • 將”shell.php 1.png”傳送至decoder模組,從text模式轉換為hex編輯模式,找到”shell.php 1.png”中空格對應的hex值“20”,將20改為00
  • 從hex模式恢復為text並將修改過的字串替換原來報文中的”shell.php 1.png”
  • 傳送報文,操作成功後會顯示檔案上傳成功

操作成功後會顯示檔案上傳成功,在php版本小於5.3.4的版本中,當Magic_quote_gpc選項為off時,可以在檔名中使用%00截斷,所以可以把上傳檔案命名a1.php%00.png進行繞過,我們用bp抓包檢測一下檔案型別。 可以發現檔案型別是png成功繞過前端,並且到伺服器檔案會被解析成php檔案,因為00後面的被截斷了,伺服器不解析。

但是在本例中,00截斷的方法不再有效,因為if條件中還有一個getimagesize()函式,此函式會自動識別上傳的圖片的檔案頭,長寬,mime型別等資訊,因此如果上傳的檔案不是圖片將無法上傳。繞過這個限制的方法是製作圖片馬,我是在win環境下製作的,只需準備一個圖片大小較小的jpg或者png格式的圖片,開啟cmd使用命令:

copy 1.jpg/b+shell.php 2.jpg

來合成一張圖片馬,如果用二進位制編輯器開啟此檔案會發現一句話木馬寫到了檔案的後面,把這樣的檔案上傳時,由於檔案頭仍然是jpg的檔案頭,getimagesize()函式也會正確的返回圖片的大小和文型別,因此通這種方式可以繞過getimagesize()函式的限制,再結合00截斷即可上傳木馬並在伺服器端將檔案解析為php指令碼,從而正確執行。

但是如果伺服器的PHP版本較高,則無法通過此方法進行漏洞的利用,需要結合檔案包含漏洞進行利用。

0x03 無解的防護-全方面限制

當然安全只是相對的,沒有絕對的安全,一下程式碼對輸入的檔案進行了多種方式的審查並進行了重新編碼,是目前比較完善了安全防禦措施。

原始碼 upload_2.php

<?php
if( isset( $_POST[ 'Upload' ] ) ) {
    // 檢查token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
    $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
    $uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
    $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
    $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
    $uploaded_tmp  = $_FILES[ 'uploaded' ][ 'tmp_name' ];
    $target_path   = 'uploads/';
    $target_file   =  md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
    $temp_file     = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) );
    $temp_file    .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
    //判斷是否是一張圖片
    if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' ) &&
        ( $uploaded_size < 100000 ) &&
        ( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) &&getimagesize( $uploaded_tmp ) ) {
        //重新制作一張圖片,抹去任何可能有危害的資料
        if( $uploaded_type == 'image/jpeg' ) {
            $img = imagecreatefromjpeg( $uploaded_tmp );
            imagejpeg( $img, $temp_file, 100);
        }
        else {
            $img = imagecreatefrompng( $uploaded_tmp );
            imagepng( $img, $temp_file, 9);
        }
        imagedestroy( $img );
        //檔案轉儲
        if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) {
            $html .= "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>";
        }
        else {
            $html .= '<pre>Your image was not uploaded.</pre>';
        }
        //刪除所有暫時檔案
        if( file_exists( $temp_file ) )
            unlink( $temp_file );
    }
    else {
        //無效檔案
        $html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
    }
}
// 新增抗csrf驗證
generateSessionToken();
?>

上述程式碼的安全措施:

  • 新增了sessionToken,驗證會話身份,用於防止csrf攻擊
  • 使用md5( uniqid() . $uploaded_name )函式,uniqid()函式是根據當前的時間,生成一個唯一的id,跟大多數隨機函式一樣,基於時間的隨機函式在一定條件下也是可以差生碰撞的,因此本例中採用了md5()函式來保證生成id的唯一性,而且由於md5()函式對上傳的檔名進行了重新命名,因此無法使用00截斷的方式來上傳php或者其他惡意指令碼檔案。
  • 以白名單的方式限制上傳的檔案字尾
  • 限定上傳的檔案大小不得超過10000
  • 通過imagecreatefromjpeg()和imagecreatefrompng()函式將上傳的圖片檔案重新寫入到一個新的圖片檔案中,這兩個函式會自動將圖片中的有害後設資料抹除,因此即使黑客上傳了一張圖片馬也會被這個函式過濾成一個純正的圖片。
  • imagedestroy( $img )將使用者上傳的原始檔刪除
  • unlink( $temp_file )刪除過濾過程中產生的任何臨時檔案

0x04 個人總結

web漏洞種類繁多,利用方法奇葩而有趣,值得研究和學習

相關文章