thinkphp反序列化漏洞核心在于实操判断触发点、pop链走向及可用类,需聚焦unserialize()显式调用入口(如php://input、get/post解码后、session反序列化),结合model::__destruct→save→__tostring链,利用conversion等含__tostring的类达成rce。

ThinkPHP反序列化漏洞不是“有没有”的问题,而是“在哪触发、链怎么走、哪些类能用”的实操判断问题。直接看入口和链,比泛泛谈原理更省时间。
找 unserialize() 入口点必须盯死这三处
框架本身不主动调用 unserialize(),漏洞一定来自开发者代码或第三方扩展的显式调用。重点审计以下位置:
-
file_get_contents('php://input')+unserialize()组合(常见于 API 接口、Webhook 回调) -
$_GET/$_POST中经base64_decode()或urldecode()后传入unserialize() - Session 反序列化场景:当
session.serialize_handler被设为php_serialize,且攻击者能控制 session 内容(如通过var_dump()泄露、Redis 注入等),再由session_start()触发反序列化
注意:think\Container::make() 或 app()->make() 不会直接反序列化,但若其参数来自用户输入且未过滤,可能间接引入可控对象实例 —— 这类路径要结合依赖注入逻辑追,不能只扫函数名。
ThinkPHP 6.x 的关键 POP 链起点是 Model::__destruct()
在 vendor/topthink/think-orm/src/Model.php 中,__destruct() 方法会无条件调用 $this->save(),只要 $this->lazySave === true(该属性可被反序列化控制):
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}
继续跟进 save() → updateData() → checkAllowFields() → db(),最终在 db() 中将 $this->table 当作字符串拼接,触发 __toString()。这意味着:只要你能控制一个 think\Model 子类实例的 $table 属性为另一个含 __toString() 的对象,链就通了。
典型可用终点类包括:
-
think\model\concern\Conversion(含__toString()→toJson()→toArray(),可进一步触发getAttr()和__call()) -
think\route\RuleItem(部分版本中__toString()调用buildUrl(),可带入可控参数)
别硬记类名,用 grep -r "__toString" vendor/topthink/ 本地扫一遍更快。
用 phpggc 快速验证链是否存在,别手撸 EXP
手动构造 ThinkPHP 的 POP 链效率低、易出错。直接用 phpggc 验证是否具备 RCE 条件:
- 查支持链:
php phpggc -l ThinkPHP - 看某条链细节:
php phpggc -i ThinkPHP/RCE1(确认适用版本是否匹配你审计的目标) - 生成基础 payload:
php phpggc ThinkPHP/RCE1 system "id"
如果命令执行成功,说明链可用;如果报错 Class not found,大概率是目标环境没装对应组件(如 think-orm 未启用、或用了精简版框架)。此时需回退到源码层确认类是否存在,而不是强行改链。
注意:phpggc 生成的 payload 默认未编码。若目标接收的是 base64 参数,必须加 --base64;若走 URL 传参,加 --url,否则 +、/ 等字符会被截断或转义。
__wakeup() 跳过和属性可见性是绕过防御的关键细节
很多补丁只加了 if (method_exists($obj, '__wakeup')) { $obj->__wakeup(); } 这类检查,但忽略了一个事实:PHP 在反序列化时,即使类里没定义 __wakeup(),也不会报错,只是跳过。所以“删掉 __wakeup()”不是修复,只是让链变短。
更隐蔽的坑在属性可见性上:
- public 属性在序列化字符串中明文出现,如
s:3:"uid";i:1024 - protected 属性带不可见字符
\0*\0email,URL 传输时极易被中间件(Nginx、WAF)截断或丢弃 - private 属性带
\0类名\0,同样存在编码兼容问题
这意味着:你在本地 serialize() 出来的字符串,直接贴到 URL 里十有八九失败。必须用 base64_encode() 包一层,或改用 POST body 传原始二进制数据 —— 这个细节卡住过太多人。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











