TP6.0反序列化利用链挖掘思路总结
字数 1678 2025-08-26 22:11:40

ThinkPHP 6.0 反序列化利用链挖掘与分析

基础知识

1. PHP反序列化

  • 序列化:将PHP值转换为可存储或传输的字符串,目的是防止丢失其结构和数据类型
  • 反序列化:序列化的逆过程,将字符串再转化成原来的PHP变量
  • 涉及函数:serialize()unserialize()

2. PHP魔术方法

魔术方法是以两个下划线字符(__)开头的方法,由特定条件触发执行:

魔术方法 触发条件
__destruct 明确销毁对象或脚本结束时
__wakeup 使用unserialize
__toString 类被转换成字符串时
__call 调用不可访问或不存在的方法时
__callStatic 调用不可访问或不存在的静态方法时
__set 给不可访问或不存在属性赋值时
__get 读取不可访问或不存在属性时
__isset 对不可访问或不存在的属性调用isset()empty()
__invoke 以函数方式调用对象时

3. 反序列化漏洞利用过程

  • 起点:反序列化时触发的魔术方法(如__destruct__wakeup
  • 中间跳板:字符串操作触发的魔术方法(如__toString__call
  • 终点:最终达到的代码执行点(如call_user_funccall_user_func_array

利用链挖掘与分析

1. 环境搭建

composer create-project topthink/think TP-6.0 6.0.*-dev

2. 利用链一:Model类入口

起点:Model类的__destruct

// vendor/topthink/think-orm/src/Model.php
public function __destruct()
{
    if ($this->lazySave) { // 需要$this->lazySave为true
        $this->save();
    }
}

触发条件链

  1. $this->save()需要满足:

    • $this->isEmpty()为false → $this->data不为空
    • $this->trigger('BeforeWrite')为true → $this->withEvent为false
    • $this->exists为true
  2. 进入updateData()方法:

    • $this->force为true
    • $this->field为空
    • $this->schema为空
  3. 触发db()方法中的字符串拼接:

    • $this->connection为"mysql"

中间跳板:__toString

// vendor/topthink/think-orm/src/model/concern/Conversion.php
public function __toString()
{
    return $this->toJson();
}

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

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

终点:动态代码执行

// vendor/topthink/think-orm/src/model/concern/Attribute.php
public function getAttr(string $name)
{
    return $this->getValue($name, $value, $relation);
}

protected function getValue(string $name, $value, bool $relation = false)
{
    $closure = $this->withAttr[$fieldName];
    $value = $closure($value, $this->data); // 动态函数执行
}

3. 利用链二:AbstractCache类入口

起点:AbstractCache类的__destruct

// vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php
public function __destruct()
{
    if (!$this->autosave) { // $this->autosave=false
        $this->save();
    }
}

触发条件链

  1. 子类CacheStore的save()方法:

    // vendor/topthink/framework/src/think/filesystem/CacheStore.php
    public function save()
    {
        $contents = $this->getForStorage();
        $this->store->set($this->key, $contents, $this->expire);
    }
    
    • $this->store可控,可调用任意类的set方法
    • $this->key$this->expire可控
  2. 调用File类的set()方法:

    // vendor/topthink/framework/src/think/cache/driver/File.php
    public function set($name, $value, $expire = null): bool
    {
        $data = $this->serialize($value);
    }
    
  3. 触发serialize()中的动态代码执行:

    // vendor/topthink/framework/src/think/cache/Driver.php
    protected function serialize($data): string
    {
        $serialize = $this->options['serialize'][0]; // 可控
        return $serialize($data); // 命令执行点
    }
    

4. 利用链三:任意文件写入

基于利用链二,利用File类的set()方法中的文件写入功能:

$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);

绕过死亡exit的方法:

  • 使用php://filter协议的base64编码

思路总结

  1. 反序列化利用链基础挖掘思路

    • 先找到入口文件(通常是__destruct__wakeup
    • 层层跟进,找到代码执行点等危险操作
    • 特别注意魔术函数、任意类和函数的调用、以及子类的综合分析
  2. 构造POC注意事项

    • Trait类无法直接实例化,需要找到复用它的类
    • 抽象类需要找到子类普通类来实例化
    • 注意ThinkPHP的命名空间问题
  3. 命名空间引用方式

    • 非限定名称:new foo()
    • 限定名称:new subnamespace\foo()
    • 完全限定名称:new \currentnamespace\foo()
  4. POC构造示例

namespace MyTest1{
    class Test {}
}

namespace MyTest2{
    class Test {}
}

namespace{
    $v = new MyTest2\Test();
    $s = new MyTest1\Test();
    $this->xxx = $v;
    echo serialize($s);
}

参考

  1. 挖掘暗藏ThinkPHP中的反序列利用链
  2. ThinkPHP6.X反序列化利用链
  3. ThinkPHP 6.0.x反序列化(二)
  4. PHP手册-命名空间
ThinkPHP 6.0 反序列化利用链挖掘与分析 基础知识 1. PHP反序列化 序列化 :将PHP值转换为可存储或传输的字符串,目的是防止丢失其结构和数据类型 反序列化 :序列化的逆过程,将字符串再转化成原来的PHP变量 涉及函数: serialize() 和 unserialize() 2. PHP魔术方法 魔术方法是以两个下划线字符( __ )开头的方法,由特定条件触发执行: | 魔术方法 | 触发条件 | |---------|---------| | __destruct | 明确销毁对象或脚本结束时 | | __wakeup | 使用 unserialize 时 | | __toString | 类被转换成字符串时 | | __call | 调用不可访问或不存在的方法时 | | __callStatic | 调用不可访问或不存在的静态方法时 | | __set | 给不可访问或不存在属性赋值时 | | __get | 读取不可访问或不存在属性时 | | __isset | 对不可访问或不存在的属性调用 isset() 或 empty() 时 | | __invoke | 以函数方式调用对象时 | 3. 反序列化漏洞利用过程 起点 :反序列化时触发的魔术方法(如 __destruct 、 __wakeup ) 中间跳板 :字符串操作触发的魔术方法(如 __toString 、 __call ) 终点 :最终达到的代码执行点(如 call_user_func 、 call_user_func_array ) 利用链挖掘与分析 1. 环境搭建 2. 利用链一:Model类入口 起点:Model类的 __destruct 触发条件链 $this->save() 需要满足: $this->isEmpty() 为false → $this->data 不为空 $this->trigger('BeforeWrite') 为true → $this->withEvent 为false $this->exists 为true 进入 updateData() 方法: $this->force 为true $this->field 为空 $this->schema 为空 触发 db() 方法中的字符串拼接: $this->connection 为"mysql" 中间跳板: __toString 终点:动态代码执行 3. 利用链二:AbstractCache类入口 起点:AbstractCache类的 __destruct 触发条件链 子类CacheStore的 save() 方法: $this->store 可控,可调用任意类的 set 方法 $this->key 和 $this->expire 可控 调用File类的 set() 方法: 触发 serialize() 中的动态代码执行: 4. 利用链三:任意文件写入 基于利用链二,利用File类的 set() 方法中的文件写入功能: 绕过死亡exit的方法: 使用 php://filter 协议的base64编码 思路总结 反序列化利用链基础挖掘思路 : 先找到入口文件(通常是 __destruct 或 __wakeup ) 层层跟进,找到代码执行点等危险操作 特别注意魔术函数、任意类和函数的调用、以及子类的综合分析 构造POC注意事项 : Trait类无法直接实例化,需要找到复用它的类 抽象类需要找到子类普通类来实例化 注意ThinkPHP的命名空间问题 命名空间引用方式 : 非限定名称: new foo() 限定名称: new subnamespace\foo() 完全限定名称: new \currentnamespace\foo() POC构造示例 : 参考 挖掘暗藏ThinkPHP中的反序列利用链 ThinkPHP6.X反序列化利用链 ThinkPHP 6.0.x反序列化(二) PHP手册-命名空间