ThinkPHP反序列化链构造的核心是串联多个类的魔术方法形成可控执行路径,以触发命令执行、文件写入或删除等危险操作;起点多为__destruct/__wakeup,中间依赖__toString/__invoke跳转,终点常落于__call或system等危险函数调用。

ThinkPHP反序列化链构造的核心,是把框架中多个类的魔术方法串联成一条可控的执行路径,最终触发危险操作,比如命令执行、文件写入或删除。关键不在于单个类有多“危险”,而在于它们如何被拼接——属性赋值、方法调用、类型转换这些看似平常的操作,恰恰构成了POP链的齿轮。
从入口点开始:__destruct 和 __wakeup 是最常见起点
绝大多数ThinkPHP反序列化漏洞都以 __destruct() 或 __wakeup() 为起点。这两个方法在反序列化后自动触发,无需额外调用。比如:
- ThinkPHP 5.1.x 中
think\process\pipes\Windows类的__destruct()会调用removeFiles(),而$this->files是可控的私有属性,可被设为任意文件路径数组,导致任意文件删除; - ThinkPHP 6.0 某些版本中,
think\cache\driver\File的__destruct()会尝试写入缓存文件,若$this->options['path']和$this->key可控,就可能造成目录穿越或Webshell写入。
中间跳转靠 __toString() 和 __invoke()
当一个类的 __destruct() 不直接执行危险操作,而是返回或拼接某个对象时,就常依赖 __toString() 进行过渡。只要该对象在字符串上下文中被使用(如 echo $obj、print_r($obj)、拼接进日志等),就会触发。
- 例如,在 ezpop 类题型中,
crow的__destruct()返回$this->v1,而$this->v1是一个what实例;what类没有__toString(),但若它被强制转为字符串(比如参与==比较或日志拼接),就会走到下一个环节; -
__invoke() 则用于“函数式跳转”:当一个对象被当作函数调用(
$obj())时触发。这在容器类或门面(Facade)设计中很常见,比如think\Facade子类重写了__invoke()去代理调用真实类方法,攻击者可借此跳转到另一个存在危险逻辑的类。
终点通常落在 __call() 或危险方法调用上
链的末端往往需要触发一次“不存在的方法调用”,从而激活 __call()。这个方法的参数 $name 和 $arguments 通常是可控的,如果内部直接用于函数反射或回调,风险极高。
立即学习“PHP免费学习笔记(深入)”;
- ThinkPHP 3.2.3 的经典链:
Imagick->__destruct()→Memcache->__construct()→Model->__construct()→Mysql->query(),其中Model构造时若传入恶意connection配置,就能让Mysql执行任意SQL; - 某些链末尾会落到
call_user_func_array()、system()、file_put_contents()等函数上,只要其参数能通过前面的属性逐层传递过来,就能完成利用。
实战构造要盯住三件事:可控属性、调用链路、类型约束
写一个可用的POC不是堆砌类,而是精确控制每一步的输入输出:
-
可控属性:确认哪些属性是
public或可通过反序列化注入的private/protected(注意序列化字符串中的属性名格式,如"%00*%00files"); -
调用链路:用调试器(Xdebug)或
php_unserialize_chain_tool插件跑一遍,验证每个魔术方法是否真被调用、参数是否按预期传递; -
类型约束:ThinkPHP大量使用类型声明(如
function foo(Manager $m)),如果链中某步要求参数是特定类实例,就不能随便换;此时需找同类继承结构或兼容接口。



















