thinkphp反序列化pop链能否跑通,关键在于属性是否可控及跳转是否被中断;tp 5.x/6.x 均以__destruct为稳定起点,如windows类的removefiles()和psr6cache的save(),依赖$file、$pool等可反序列化赋值的私有属性,并通过file_exists()触发__tostring实现跳转。

ThinkPHP反序列化POP链能跑通,关键不在“有没有魔术方法”,而在“属性能不能被反序列化赋值”和“中间跳转会不会因类型/访问控制中断”。很多链本地调试成功,一放到真实TP环境就失效,问题往往出在私有属性名编码、autoload路径缺失或__call参数校验上。
ThinkPHP中__destruct是POP链最稳的起点
TP 5.x 和 6.x 的多个类都把危险操作放在 __destruct 里,比如 think\process\pipes\Windows 的 removeFiles()、Psr6Cache 的 save()。它们共同特点是:不依赖用户显式调用,unserialize() 一结束就自动触发,且入口参数(如 $this->files、$this->pool)通常可被反序列化字符串直接控制。
- TP 5.1.37 常见起点是
think\process\pipes\Windows,其$files属性为数组,removeFiles()会遍历并传给file_exists() - TP 6.x 更多用
Psr6Cache类,__destruct→save()→$this->pool->save(),只要$this->pool和$this->key可控,就能导向后续利用点 - 注意私有属性名格式:
$this->files在 Windows 类中实际是"\0think\process\pipes\Windows\0files",少一个不可见字符就会反序列化失败
file_exists() 触发 __toString 是链路关键跳板
当 file_exists($obj) 被调用时,PHP 会强制将 $obj 转为字符串,从而触发 __toString()。这不是推测——它是 PHP 强制类型转换规范行为,只要目标对象没实现 __toString 就会报错,实现了就一定会进。
- 常见承接类是
think\model\concern\Conversion(TP 5.x)或think\Model子类,其__toString()内部调用toJson()→toArray()→getAttr() -
getAttr()中若存在$closure($value, $this->data)这类调用,且$closure来自可控数组(如$this->withAttr),就可能执行system、file_put_contents等回调 - 别硬套公开 EXP:TP 6.0 的
Model类对$this->withAttr做了 callable 类型检查,直接塞字符串"system"会中断链路,得构造合法 Closure 对象
__call() 是最后落地执行的高危出口
不是所有 __call 都能 RCE,只有那些把参数原样喂给 call_user_func_array() 或 invoke() 的才可用。ThinkPHP 里真正能打的,集中在 think\Request 和部分模型扩展类中。
-
think\Request的__call()会把方法名和参数传给$this->method($args),如果$this->method被设为system,且$args是["id"],就等价于system("id") - 但要注意:TP 6.x 默认禁用了
Request类的反序列化入口,必须先通过其他类(如ResourceRegister)把控制权导过去 - 构造时避免用
__invoke():它要求对象被当作函数调用(如$obj()),而反序列化后没人主动调它;__call和__toString的触发条件更宽松、更可靠
真正卡住人的地方,从来不是“哪个类有 __destruct”,而是 $this->pool 的命名空间有没有写对、__toString 返回的是否真是字符串、__call 的第一个参数是不是被框架中间件提前过滤掉了——这些细节不调试到断点里,光看源码根本发现不了。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











