看 PHP 漏洞代码光看源码很容易看晕——调用栈深、魔术方法乱跳。我的解法是把调试环境搭起来单步跟,变量怎么变、链子怎么走一目了然。这篇是我搭 Xdebug 环境和用它跟踪 ThinkPHP 反序列化链的记录。
环境清单
| 组件 | 版本/说明 |
|---|---|
| phpStudy | PHP 7.3 + 自带 Xdebug 扩展 |
| 靶场 | ThinkPHP 5.1.29(tpdemo5129),跑在 8002 端口 |
| 编辑器 | VSCode + PHP Debug 扩展(xdebug.php-debug) |
| 调试点 | 127.0.0.1:8002/index/index/index?c=<payload> |
Xdebug 配置
php.ini 里的关键几行(Xdebug 3.x 写法):
[Xdebug]
zend_extension=xdebug
xdebug.mode = debug
xdebug.client_host = 127.0.0.1
xdebug.client_port = 9000
xdebug.start_with_request = yes
start_with_request = yes 表示每个请求都尝试连调试器——调试期方便,平时记得关。
VSCode 侧
launch.json 只需要一个 Listen for Xdebug 配置,端口对到 9000。启动监听后,浏览器(或 curl)发请求,Xdebug 就会主动连过来。
验证连通性的土办法:抓包看 dbgp 握手——监听起来后第一个数据包是 Xdebug 发的 <init ...> XML,看到这个就说明链路通了。我第一次配的时候端口被别的进程占了,半天没反应,就是靠抓包定位的。
实战:单步跟踪 TP 5.1.29 反序列化链
入口 payload 触发 unserialize() 后,我在三个关键位置下断点:
Windows.php:163 __destruct() 里 removeFiles() 的调用点
Request.php:331 魔术方法跳板
Conversion.php:192 __toString() 最终拼接点
跟踪过程里真正值钱的观察:
__destruct不是起点而是引爆点:对象销毁时自动触发,POP 链的第一环从这里挂上。- 变量在哪一步被污染:单步到
removeFiles()时 watch 窗口能直接看到攻击者控制的属性值流进文件操作函数。 - 跳板方法的寻找思路:在调用处”步入”几次就能摸清这条链为什么选这几个类——每个跳板都只是”把可控属性传给下一个魔术方法”。
跟完一遍之后再看 POP 链的 payload 构造,就不是背链子,而是知道每一环为什么这么串。
踩坑
- 9000 端口无响应 → 先查端口占用,再抓包看有没有 dbgp 握手,别瞎改配置。
- 断点不命中 → 确认请求的路由真的会走到那个类(TP 的命名空间映射先看懂)。
- php.ini 改了没生效 → phpStudy 要重启对应 PHP 服务,phpinfo() 里搜 xdebug 确认。
- Xdebug 2.x 和 3.x 配置项完全不同(
remote_enablevsmode),对着版本写。
小结
源码审计 + 动态调试是两条腿:审计帮你猜链子在哪儿,调试帮你验证链子怎么走。这套环境搭一次大概二十分钟,之后所有 PHP 漏洞分析都能复用。