前言
前段时间参加了信息安全国赛,其中有一个题是
ThinkPHP6
反序列化,当时只是做了一些简单的审计,最后直接用网上的EXP
打。现在比赛结束了,所以特地花点时间重新审计一番,小记一下。
1. 利用链分析
1.1 反序列化的思考
在PHP中,反序列化分为有类和无类两种,无类的反序列化利用比较简单直接。有类的时候一般需要挖掘利用链,其中起到关键作用的就是魔术方法,魔术方法是连接利用链的桥梁。
在挖掘反序列化的时候,一般来说首先要找的就是__destruct()
,__wakeup
两个函数
__destruct() // 对象被销毁的时候调用
__wakeup() // 反序列化的时候调用
1.2 寻找触发条件
废话不多说了,知道了入口点以后,现在开始漏洞挖掘,首先寻找入口点
__destruct()
- 用
Seay
全局搜索存在__destruct()
的地方
- 然后找可利用的点,在
/vendor/topthink/think-orm/src/Model.php
中发现__destruct()
调用了save()
方法,只需要$this->lazySave
为true
即可,并且这个参数是可控的
/**
* 析构方法
* @access public
*/
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}
- 跟进
save()
方法,关键点如下,需要进入到updateData()
函数,要绕过位置1处的判断和$this->exists
为true
$this->exists
是可控的,所以关键点是绕过前面的判断
接下来查看
$this->isEmpty()
函数和$this->trigger()
函数
- 查看
isEmpty()
函数,使用empty()
函数对$this->data
进行判断,不为空即可,且参数可控
trigger()
函数默认返回的是true
,所以也能绕过
- 接下来跟进
updateData()
方法,只要前面的绕过了,就会来到如下位置 - 所以
$this->checkAllowFields()
默认就会被调用,继续跟进
- 跟进
checkAllowFields()
函数,$this->field
参数和$this->schema
默认为空数组,所以默认会直接调用db()
函数
- 继续跟进
db()
函数,这里的$this->table
是可控的,所以可以进入到位置2,并且这里将$this->table
当作了字符串使用,如果$this->table
是一个对象,那么就可以触发__toString()
魔术方法的执行
1.3 __toString() 挖掘利用
上面的分析找到通过
__destruct()
一步步触发执行__toString()
,接下来继续寻找__toString()
函数中可用于利用的点
- 还是全局搜索
__toString()
,在/vendor/topthink/think-orm/src/model/concern/Conversion.php
中找到一个__toString()
方法,里面调用了toJson()
方法
- 跟进
toJson()
方法,这里调用了toArray()
方法
- 跟进
toArray()
方法,关键代码如下,这里将$this->data
和$this->relation
两个数组合并,然后进行遍历,在默认情况下会调用到最下面的$this->getAttr($key)
,其中$this->data
是可控的,所以这里传入的$key
也能被控制
- 跟进
getAttr()
函数,$relation
默认为false
,然后$value
的值从getData()
函数中获取,然后将参数传入getValue()
函数中
- 先查看一下
getData()
函数做了什么,这里调用了getRealFieldName()
函数(实际上只是对内容进行了一下检测而已),只要内容合法,那么最后返回的$fieldName
和$name
实际上是一样的 - 然后
array_key_exists()
函数判断检测后的内容在$this->data
中是否存在,实际上可以理解为检测$name
而已,只要内容合法,那么就一定存在,所以最后能执行位置2