ThinkPHP反序列化POP链本质是通过可控入口串联魔术方法(如__destruct→file_exists→__toString→__call)逐步跳转,最终触发任意代码执行或文件操作;关键在于属性可控性与自动调用的接力逻辑。

think\process\pipes\Windows::__destruct 触发 file_exists 强制类型转换
这是 ThinkPHP 5.x 和 6.x 中最稳定、复现率最高的反序列化入口点之一。关键在于 __destruct 中调用 removeFiles(),而该方法内部存在 file_exists($filename) ——当 $filename 是一个对象时,file_exists 会尝试将其转为字符串,从而强制触发 __toString()。
常见错误现象:Warning: file_exists() expects parameter 1 to be a valid path, string given,说明类型转换失败,通常是因为没控制好 $this->files 属性或私有属性名拼写错误(如漏掉 \0think\process\pipes\Windows\0files 中的不可见字符)。
实操要点:
-
$this->files必须是数组,且至少一个元素为可控对象(如伪造的think\model\Pivot或think\cache\driver\File实例) - 私有属性需严格按格式构造:
s:15:"\0think\process\pipes\Windows\0files";a:1:{i:0;O:... - 避免在链中引入依赖容器或上下文的方法(如
app()、config()),否则容易因环境缺失中断
think\cache\driver\Psr6Cache::__destruct 调用 save 导致任意写入
Psr6Cache 类在 TP6 中常作为 POP 链跳板,其 __destruct 直接调用 $this->pool->save(...),而 $this->pool 和 $this->key 均为可序列化控制的属性。只要让 $this->pool 指向一个能执行写操作的缓存驱动(如 File 驱动),再配合 $this->key 控制文件路径,就可能实现任意文件写入。
立即学习“PHP免费学习笔记(深入)”;
使用场景:目标允许反序列化且已加载 PSR-6 兼容缓存组件(如 symfony/cache 或框架内置适配器)。
参数差异与风险:
-
$this->pool若设为think\cache\driver\File实例,需确保其$this->options['path']可控,否则写入位置不可预测 -
$this->key若含目录遍历(如../../public/shell.php),需确认目标缓存驱动未对 key 做路径规范化过滤 - TP6.0.12+ 对
Psr6Cache的save()增加了类型校验,若传入非CacheItemInterface实例会抛出异常,需构造合法 item 对象
think\model\Model::save 方法绕过判断触发 updateData
Model 类的 save() 是 TP5/6 中高频利用点,但实际触发 updateData() 需满足多个条件:必须让 $this->exists === true、$this->data 非空、$this->trigger('BeforeWrite') 返回 true(默认为 true)、且 isEmpty() 返回 false。
容易踩的坑:
- 直接设
$this->exists = true不够,部分版本会检查$this->table和主键字段是否已初始化,否则在updateData()中提前退出 -
getChangedData()默认返回空数组,需通过设置$this->data和$this->origin差异来生成变更数据,否则$data为空导致后续跳转失败 - 若链最终走向
db()->query(),要注意$this->table是否被硬编码或受命名空间影响(如think\model\Pivot的表名逻辑特殊)
__toString → toJson → getRelation → __call 形成回调执行闭环
这条链常见于 TP5.1.x 和早期 TP6,核心路径是:某对象触发 __toString() → 调用 toJson() → toArray() 中调用 getRelation($key) → 若 $key 是恶意字符串且 getRelation 内部调用 $this->$key(),而该方法不存在,则触发 __call() → 若 __call() 将参数传给 call_user_func_array(),即可执行任意回调。
性能与兼容性影响:
- 该链依赖
getRelation()的具体实现,TP6.0.10+ 已将部分关系调用改为静态方法或增加类型断言,导致链断裂 -
__call()的参数传递方式因类而异:有些只传$method,有些传[$method, $args],需逐个验证源码 - 若目标开启了 OPcache 或 JIT,某些魔术方法调用可能被优化跳过,建议在复现前关闭相关优化
__destruct,而是让整条链上每个中间对象的属性都可控、每个跳转条件都可绕过、每个魔术方法调用都不因类型或上下文报错而中断——尤其是私有属性名里的不可见字符、autoload 加载失败、以及新版框架悄悄加上的类型约束。



















