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_func、call_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();
}
}
触发条件链
-
$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
// 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();
}
}
触发条件链
-
子类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可控
-
调用File类的
set()方法:// vendor/topthink/framework/src/think/cache/driver/File.php public function set($name, $value, $expire = null): bool { $data = $this->serialize($value); } -
触发
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编码
思路总结
-
反序列化利用链基础挖掘思路:
- 先找到入口文件(通常是
__destruct或__wakeup) - 层层跟进,找到代码执行点等危险操作
- 特别注意魔术函数、任意类和函数的调用、以及子类的综合分析
- 先找到入口文件(通常是
-
构造POC注意事项:
- Trait类无法直接实例化,需要找到复用它的类
- 抽象类需要找到子类普通类来实例化
- 注意ThinkPHP的命名空间问题
-
命名空间引用方式:
- 非限定名称:
new foo() - 限定名称:
new subnamespace\foo() - 完全限定名称:
new \currentnamespace\foo()
- 非限定名称:
-
POC构造示例:
namespace MyTest1{
class Test {}
}
namespace MyTest2{
class Test {}
}
namespace{
$v = new MyTest2\Test();
$s = new MyTest1\Test();
$this->xxx = $v;
echo serialize($s);
}
参考
- 挖掘暗藏ThinkPHP中的反序列利用链
- ThinkPHP6.X反序列化利用链
- ThinkPHP 6.0.x反序列化(二)
- PHP手册-命名空间