thinkphp6.0反序列化利用链审计

前言

这几天在忙实验室纳新,然后就是实验室准备创办公司,需要准备好多资料233截止到今天晚上终于把tp6的手册看完了,不得不说确实看一遍就有新的收获,上次看tp5手册遗漏了很多细节或者自己忘了233.看完手册就开始审计吧。
先上参考文章,大师傅就是大师傅啊。

https://www.anquanke.com/post/id/187393#h2-1

https://www.anquanke.com/post/id/194036

环境

tp6安装:

composer create-project topthink/think=6.0.x-dev v6.0

手动设置漏洞点:
别问为什么这样写。。这个反序列化漏洞想要触发就得有内容完全可控的反序列化点,例如: unserialize(可控变量)。
得有程序员这样写才能触发。。

将 app/controller/Index.php 代码修改成如下:


<?php
namespace appcontroller;
use appBaseController;
class Index extends BaseController
{
    public function index()
    {
        return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) 2020新春快乐</h1><p> ThinkPHP V' . 	hinkfacadeApp::version() . '<br/><span style="font-size:30px;">14载初心不改 - 你值得信赖的PHP框架</span></p><span style="font-size:25px;">[ V6.0 版本由 <a href="https://www.yisu.com/" target="yisu">亿速云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ee9b1aa918103c4fc"></think>';
    }
    public function hello($name = 'ThinkPHP6')
    {
        return 'hello,' . $name;
    }
    public function unser()
    {
        $payload = unserialize(base64_decode($_GET['p']));
    }
}


反序列化利用链

根据师傅们的文章

在 ThinkPHP5.x 的POP链中,入口都是 thinkprocesspipesWindows 类,通过该类触发任意类的 __toString 方法。但是 ThinkPHP6.x 的代码移除了 thinkprocesspipesWindows 类,而POP链 __toString 之后的 Gadget 仍然存在,所以我们得继续寻找可以触发 __toString 方法的点。
这里5.2.x版本函数动态调用的反序列化链后半部分,还可以利用。
可惜我没审计过5.2x的tp链,不过看了一下和tp5.1的链子很像,松了一口气,不然我就是一起审计2个版本了。。

照例根据之前的思路首先全局搜索__destruct:
这里就是我们的链子起点:
vender/topthink/think-orm/src/model.php

这里lazySave简单可控

我们让lazySave方法返回true即可调用save方法
我们看一下sava方法

可用的方法在updateData,首先我们要避免上面的return false,

分析一下代码:
我们为了执行到updateData方法,不能让前面直接return,即 return false
我们需要

1.$this->isEmpty()返回值为 false
2.$this->trigger('BeforeWrite')返回值为true

去看一下isEmpty()和trigger这2个方法

 public function isEmpty(): bool
    {
        return empty($this->data);
    }

-------------------------------------


 public function trigger($event, $params = null, bool $once = false)
    {
        if (!$this->withEvent) {
            return;
        }

我们可以这样:

构造$this->data为非空数组
构造$this->withEvent为false(简单可控)
构造$this->exists为true

这样成功触发updateData方法,我们去看一下:

分析代码:

if (false === $this->trigger('BeforeUpdate')) {
   这里我们上面就已经分析过了$this->trigger简单可控$this->trigger为true即可绕过继续执行下面代码

接下来同样要保证不被return,就是要保证这个$data的输出是一个非空数组
我们看一下$this->getChangedData方法

这里$this->force简单可控,只要设为true,就可以返回之前设为非空数组的$data了。
进到checkAllowFields方法,我们去看一下:

这里要触发的地方是$this->db()方法,所以要$this->field空,且$this->schema也空,默认为空。
我们看一下db方法:

关键点就是这2行了,同时这里就是可以触发__toString的位置

这里调用的db方法中用到了.的拼接,只要把$this->name或者$this->suffix设为下一个类对象,就可以触发__toString方法了。

不过为了达到该出拼接,我们还是得首先满足connect函数的调用。这里唯一的条件就是
$this->connection为mysql语句。
接下来就是和5.2的链子中__toString方法之后一样,虽然是说和5.1类似但是还是有不小区别,5.1中我们使用了一个__call方法,但在5.2版本中不存在这样的一个__call方法,因此不能利用5.1版本中的方法,不过__call之前的方法仍然可以使用,这意味着我们需要重新找一个最终达成命令执行的函数调用或者另外找一个__call方法去代替5.1版本中的这里我们来看看动态函数调用这条链子:
思路就是利用getAttr的getValue 函数,然后$value = $closure($value, $this->data);来达成动态调用
献给利用链:


 thinkmodelconcernConversion.php - > __toString()
 thinkmodelconcernConversion->toJson()
 thinkmodelconcernConversion->toArray()
 thinkmodelconcernAttribute->getAttr()
 thinkmodelconcernAttribute->getValue()

和5.1类似:
这里用到的还是Conversion类中的__toString->toJson->toArray一条,看一下

thinkmodelconcernConversion::toArray

这里拿出来代码分析一下:

public function toArray(): array
    {
        $item       = [];
        $hasVisible = false;

        foreach ($this->visible as $key => $val) {    
         //$this->visible默认值为空,无关函数,跳过            
         ......
        }

        foreach ($this->hidden as $key => $val) {          
        //$this->hidden默认值为空,无关函数,跳过
        ......
        }

        // 合并关联数据
        $data = array_merge($this->data, $this->relation); //在poc中给了$this->data=["wtz" => "ls"],所以$data = ["wtz" => "ls"] 很明显,$this->data简单可控

        foreach ($data as $key => $val) {  //$key = wtz,$val=ls
            if ($val instanceof Model || $val instanceof ModelCollection) { //判断$val是不是这两个类的实例,不是,跳过执行下一步
                // 关联模型对象
                if (isset($this->visible[$key])) {
                    $val->visible($this->visible[$key]);
                } elseif (isset($this->hidden[$key])) {
                    $val->hidden($this->hidden[$key]);
                }
                // 关联模型对象
                $item[$key] = $val->toArray();
            } elseif (isset($this->visible[$key])) {   //$this->visible[$key]值为空不存在,跳过
                $item[$key] = $this->getAttr($key);
            } elseif (!isset($this->hidden[$key]) && !$hasVisible) {  //符合
                $item[$key] = $this->getAttr($key); //$key 也是可控的跟进getAttr,
            }
        }
       ......
    }

我们去看

thinkmodelconcernAttribute->getAttr()
 public function getAttr(string $name) //$name是上一步$key的值,我们在poc中为$name=$key='wtz'
    {
        try {
            $relation = false;
            $value    = $this->getData($name);//跟进getData可以发现,得知$value='ls'
        } catch (InvalidArgumentException $e) {
            $relation = $this->isRelationAttr($name);//默认为true
            $value    = null;
        }
        return $this->getValue($name, $value, $relation);//此时$name=‘wtz’ $value=‘ls’ $relation=false, 跟进getValue
    }

依旧是thinkmodelconcernAttribute

 
public function getData(string $name = null)//可控//$name='wtz'
    {
        if (is_null($name)) {
            return $this->data;
        }
        $fieldName = $this->getRealFieldName($name); //跟进getRealFieldName 可控。poc:得知$fieldName='wtz'
        if (array_key_exists($fieldName, $this->data)) {//可控 poc中:$this->data=['wtz'=>'ls']
            return $this->data[$fieldName];//返回'ls',回到getAttr
        } elseif (array_key_exists($fieldName, $this->relation)) {
            return $this->relation[$fieldName];
        }

thinkmodelconcernAttribute::getRealFieldName

protected function getRealFieldName(string $name): string  //可控$name='wtz'
    {
        return $this->strict ? $name : App::parseName($name); //$this->strict=$name='wtz'
    }

$this->strict为判断是否严格字段大小写的标志,默认为true,因此getRealFieldName默认返回$name参数的值,回到getData看。

我们去看thinkmodelconcernAttribute->getValue()
protected function getValue(string $name, $value, bool $relation = false)
    {                 //$name='wtz' $value=‘ls’ $relation=false
        // 检测属性获取器
        $fieldName = $this->getRealFieldName($name);  //该函数默认返回$name='wtz'=$fieldName 
        $method    = 'get' . App::parseName($name, 1) . 'Attr';  //拼接字符:getlinAttr

        if (isset($this->withAttr[$fieldName])) {  //withAttr可控['wtz'=>'system']
            if ($relation) { //$relation=false
                $value = $this->getRelationValue($name);
            }

            $closure = $this->withAttr[$fieldName]; //$closure='system'
            $value   = $closure($value, $this->data);//system('ls',$this->data),命令执行
        }
        .......
        return $value;
    }

最终在getValue处动态调用函数命令执行。
这里再来解释一下system
可以看到最终是执行了system(“ls”, ["wtz"=>"ls"]),而system函数第二个参数是可选的,也就是这种用法是合法的

注:system ( string $command [, int &$return_var ] ) : string参数

command要执行的命令。 return_var如果提供 return_var 参数, 则外部命令执行后的返回状态将会被设置到此变量中。

最后执行:
http://127.0.0.1:88/v6.0/public/index.php/index/unser?p=poc
即可成功执行命令

公网一大把poc,我也是现找的,这里我就不放了。
最后总结一下接用一下这个师傅的图:

https://xz.aliyun.com/t/6479

总结

其实综合来看,tp的反序列化链都挺鸡肋,因为利用条件苛刻,哪有程序员会这样写。。
tp系列的洞只有RCE最严重,不过现在也几乎没有了233(0day除外)
本来想多审计几条tp6的链子,但是审完一条后感觉的大同小异,链子的起点有所不同,所以这里我就不再审计了。

参考

https://www.anquanke.com/post/id/187393#h2-1

https://www.anquanke.com/post/id/194036

https://xz.aliyun.com/t/6479

原文地址:https://www.cnblogs.com/wangtanzhi/p/12913807.html