upload-labs 1-20关


简介

upload-labs是用PHP写的,收集了多种上传漏洞姿势的靶场,GitHub地址。

复现

Pass-1

pass1通过前端JavaScript代码限制了必须上传.jpg、.png、.gif的文件,先将php文件后缀改为白名单允许的文件后缀,点击上传后抓包把文件后缀改回php即可绕过。或者检查元素,在上传表单处删除onsubmit="return checkFile()",也可以修改checkFile函数,或者直接禁用JavaScript。

Pass-2

pass2在服务端对上传文件的MIME进行校验,在HTTP协议中,使用Content-Type表示文件的MIME类型,抓包修改MIME为image/jpeg、image/png、image/gif即可绕过。如下图:

接着获取上传后文件的路径,用蚁剑连接即可。

Pass-3

pass3在服务器端对文件名进行校验,黑名单为asp、aspx、php、jsp,想到了文件解析漏洞中,Apache的配置文件中有.+.php(p[345]?|t|tml)这类正则表达式,也就是说php3、php4、php5、pht、phtml也会被当做php来解析,尝试上传php3,如下图:

打开文件链接,可以看到文件名被修改,但是依然可以访问到该文件。

Pass-4

pass4将黑名单的范围进一步扩大:

但是没有对Apache的分布式配置文件.htaccess进行限制,因此上传.htaccess,设置将一个非常规后缀按php进行解析,

AddType application/x-httpd-php .xyz

再上传一个.xyz后缀的一句话木马,即可通过蚁剑连接。

Pass-5

pass5在pass4的基础上把.htaccess文件也加入了黑名单,显示源码查看其黑名单的列表,找到没有过滤的PHp后缀,大小写混合进行绕过,如下图所示:

蚁剑测试可以成功连接。前面也是可以大小写绕过的,这里还有另一种方法,禁止了.htaccess,可以尝试.user.ini;php.ini是php的配置文件,对整个web服务起作用,.user.ini是对目录起作用的配置文件,如果PHP以模块化运行在Apache中,则用.htaccess有同样的效果,也就是说如果web容器不是Apache,.uer.ini依然有效,而.htaccess不可用。

.user.ini中可定义的模式为:PHP_INI_PERDIR、PHP_INI_USER,所谓可定义的模式相当于权限,一些敏感的配置是只能在php.ini中定义的,其定义模式就为PHP_INI_ONLY,而一些比较不敏感的内容则可以在任何地方设定,其定义模式就可能为PHP_INI_ALL。

有两个配置选项需要引起重视:

auto_prepend_file在页面顶部加载文件、auto_append_file在页面底部加载文件,相当于在执行PHP文件前/后调用了require,例如我们指定auto_prepend_file=shell.php,则当我们访问某一个正常的PHP文件时,都会自动的包含shell.php。

例如我们上传一个system.jpg文件,内容是一句话木马,并上传.user.ini文件,指定auto_prepend_file=system.jpg(这里不绝对,根据system.jpg上传后的最终文件名决定),从而可以在Nginx中也能够利用。

Pass-6

pass6对其源码进行审计,发现它对文件名做的处理有:1.删除文件名末尾的点 2.以.分隔找到文件后缀 3.将后缀改为小写 4.匹配后缀的字符串::$DATA并删除。然后对后缀进行黑名单匹配,可以发现并未过滤后缀的空格,若存在空格,则可以绕过,于是:

Pass-7

pass7去除了删掉文件末尾点的函数,添加了首尾去空的函数,因此将文件名末尾的空格改为.绕过黑名单的过滤。

Pass-8

pass8去掉了这一行:$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA,在Windows中,文件名末尾加::$DATA是一种文件流的命名方式,具体可以参考微软文档文件流,用在文件上传这里,当我们把文件名末尾添加::$DATA时,会绕过对后缀的黑名单检测,而又不会影响文件的使用,依然可以通过原文件名访问到该文件。

Pass-9

pass9把前面的包括删除文件名末尾的点、后缀转换为小写、去除字符串::$DATA、首尾去空格都考虑在内了,黑名单相当全面,前面的方法似乎都行不通了,但是删除文件末尾的点函数deldot()并非是PHP官方的函数,而是作者自己写的函数,突破口就在这里。

function deldot($s){
	for($i = strlen($s)-1;$i>0;$i--){
		$c = substr($s,$i,1);						//substr(string str, int start, [int length])
		if($i == strlen($s)-1 and $c != '.'){
			return $s;
		}

		if($c != '.'){
			return substr($s,0,$i+1);
		}
	}
}

该函数从后向前遍历文件名的每一个字符,若该字符为".",则向前遍历,直到某一字符不为"."时,停止遍历,返回该字符及其向前未遍历所有字符,于是构造如qwe.php. .,注意最后两个点之间有一个空格,此时当遍历至空格时,跳出循环,返回qwe.php. ,后面又进行了对后缀的首尾去空,于是文件后缀名为.php.从而绕过了黑名单的检验。

Pass-10

pass10依旧是黑名单校验,但与之前不同的是,这里并不是去匹配黑名单,而是对文件名进行黑名单匹配替换,若匹配到黑名单中的字符串,则替换为空。于是自然可以想到使用双写进行绕过。

Pass-11

pass11没有再使用黑名单,而是通过白名单限制文件的后缀为jpg、png、gif,具体逻辑可以分析代码如下:

$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
    if(in_array($file_ext,$ext_arr)){
        $temp_file = $_FILES['upload_file']['tmp_name'];
        $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

        if(move_uploaded_file($temp_file,$img_path)){
            $is_upload = true;
        } else {
            $msg = '上传出错!';
        }
    } else{
        $msg = "只允许上传.jpg|.png|.gif类型文件!";
    }
}

strrpos函数为计算指定字符串在目标字符串中最后一次出现的位置,于是这里$file_ext变量保存的便是截取文件名中最后一次出现.的下一个位置起到文件名末尾。接着判断$file_ext是否存在于白名单中,若不存在则返回并提示只允许上传的类型,若存在于白名单中,通过get方式从客户端获取文件保存路径,将文件移动到该保存路径中。

这里关键就在于这个保存路径可控,例如我们可以将保存路径指定为../upload/shell.php%00,于是完整的保存路径将是../upload/shell.php%00<时间戳>.jpg,这里利用了PHP的一个截断漏洞,当PHP版本小于5.3.4时,当处理文件名遇到0x00时会认为是终止符,从而丢弃后面的包含.jpg在内的内容,于是我们上传的内容将会以shell.php的名字保存在服务器端。

当我们按照返回的路径查看文件时会提示not found,而此时webshell已上传成功,测试如下:

Pass-12

pass12与pass11原理相同,都是上传的路径可控,区别在于这里的保存路径以post方式传递,因此通过抓包,同11即可,要注意的是这里不能使用%00,%00是0x00URL编码的结果,POST方式不可使用,但可以直接修改抓包结果的十六进制。

Pass-13

pass13要求我们上传图片马,并结合文件包含漏洞,也就是说我们需要上传的是图片,而图片中包含一句话木马,再通过include.php将该图片包含,于是我们构造图片马成功上传即可。另外这里提示这里只检测图片的前两个字节,这里需要知道常见图片文件的特征码(首若干字节)。

PNG: 89 50 4E 47 0D 0A 1A 0A即:.PNG
JPG: FF D8
GIF: 47 49 46 38 39|37 61即:GIF89(7)a
BMP: 42 4D即:BM

于是可以直接构造如GIF89a这样的图片马,也可以通过copy命令制作,如:copy a.png/b + a.php/a shell.png如下图:

Pass-14

pass14使用getimagesize()函数检查文件是否为图片文件,通过copy制作图片马可以绕过。

Pass-15

pass15使用exif_imagetype()检查是否为图片文件,同pass14的图片马。

Pass-16

pass16对图片进行了重新渲染,当我们制作完图片马后,webshell一般被写在文件末尾,但是重新渲染后我们的木马可能会被修改。我们可以先上传图片马,上传成功后将图片下载下来,将下载后的图片与原图片进行对比,找到未修改的部分,将webshell写入未修改的部分。

Pass-17

pass17提示需要代码审计,源码如下:

$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 = '上传出错!';
    }
}

可以看到这里使用的是白名单校验,但在校验之前,先将文件保存在"upload"路径下,再判断文件后缀是否符合表名单,若符合则对文件进行改名操作,若不符合则返回提示信息并删除文件。这就带来了条件竞争的问题,因为代码的执行是需要时间的,若我们上传一个php文件,内容是想一个新的PHP文件中写入一句话木马,当我们访问到该文件时,新的webshell已被写入,因此我们可以通过bp抓包后导入Intruder模块不停向服务器发包,通过Intruder或Python脚本同时不停访问该文件,即可bypass。

如下(注意要设置为NULL payloads和Continue Indefinitely):

将线程尽可能设置高例如15、20,点击start attack后约几秒钟shell.php就被成功写入了。

Pass-18

pass18依然提示需要代码审计,查看myupload.php

其中move函数为:

  function move(){
    
    if( move_uploaded_file( $this->cls_tmp_filename, $this->cls_upload_dir . $this->cls_filename ) == false ){
      return "MOVE_UPLOADED_FILE_FAILURE";
    } else {
      return 1;
    }

  }

这里先将文件从临时目录移动到上传路径下,再对其重命名,这之间就存在了时间差,可以利用条件竞争上传。但在该函数之前存在对文件后缀名的判断,因此不能上传.php文件,可以利用Apache解析漏洞,上传.php.xyz后缀名。当Apache2.0.x<=2.0.59,Apache2.2.x<=2.2.17时,Apache解析文件的规则是从右到左,若后缀不可识别就继续往左判断。由于我本地的环境是Apache2.4,测试了下解析漏洞不成功,因此没有尝试;看网上文章另一种方法是结合文件包含,上传图片马利用include.php对其进行包含,同pass17,一边上传,一边用脚本快速访问include.php?file="文件名"

Pass-19

pass19的文件的保存名字通过客户端获取,是用户可控的,且为黑名单绕过,抓包尝试在文件名后面添加诸如.或空格(实测Windows环境下两种方案均可,Linux下后加空格可以上传成功但访问404,后加.可行);另解:由于文件名是可控的,又作为move_uploaded_file的参数,利用move_uploaded_file()函数的截断漏洞,添加00字符绕过(十六进制编辑)。

Pass-20

pass20提示来源于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 = "请选择要上传的文件!";
}

首先有一个检查MIME,这一个容易绕过;接下来判断post传递的save_name是否为空,若为空则用$_FILES['upload_file']['name']来赋值给$file,否则用save_name作为$file;接着判断若$file不是一个数组,则用.对其进行分隔并转为小写(也就是说$file即save_name是可以为数组的,并在分隔后再次变为数组)。再取$file的最后一个元素做白名单检验,在最后拼接文件名时使用了$file[count($file) - 1]

那么上传一个save_name数组,其中下标为0的元素置为shell.php,再设置save_name[2]为白名单的值,这样$file[count($file) - 1]即为$file[1],无法获取到值,reset()函数会把数组的内部指针指向第一个元素,并返回这个元素的值,即为shell.php


(但是不知为何发现这里上传成功的文件后面有一个.,试了几个例子依然如此,不过可以成功连接shell)