PHP反序列化漏洞复现关键在三步:找准入口、构造合法链、绕过长度陷阱;需验证unserialize是否真实接收输入,校准POP链中所有s:N和O:N的字节数,注意PHP版本差异及传输编码问题。

PHP反序列化漏洞复现不是拼运气,关键在三步:找准入口、构造合法链、绕过长度陷阱。很多初学者卡在“页面没反应”或“报错但不执行”,其实90%是序列化字符串格式出错,尤其是属性名长度、类名长度、嵌套层级的字节计数偏差。
确认unserialize入口是否真正接收用户输入
先别急着写payload,用最简方式验证目标是否真的在反序列化你传的数据。常见误区是看到unserialize($_GET['data'])就开干,但实际代码可能加了trim()、base64_decode()、json_decode()等中间处理,或者用了filter_input做了类型转换。
- 在靶场中访问
?data=a:1:{s:2:"aa";s:2:"bb";},看是否报Notice: unserialize(): Error at offset——有报错说明确实进了unserialize - 若返回空白或500,尝试加
die('hit');到unserialize前,确认参数是否被正确读取 - 注意PHP版本差异:7.4+对非法序列化字符串默认静默失败,不会报错,需配合error_log或xdebug观察
手工构造POP链时必须校准每一处长度
序列化字符串里所有s:N:"xxx"、O:N:"Class"中的N都是**字节数**,不是字符数。尤其遇到中文、emoji、protected/private属性时极易翻车。
- public属性:
s:4:"name"→ name是4个ASCII字节,没问题 - protected属性:
s:8:"%00*%00name"→ 实际是\x00*\x00name共8字节(\x00占1字节,*占1字节,name占4字节) - private属性:
s:12:"%00User%00name"→ \x00 + 类名 + \x00 + 属性名,User是4字节,所以是1+4+1+4=10?不对——User是UTF-8编码,但类名长度N只算字节数,不是字符数;若类名含中文,必须用strlen()而非mb_strlen() - 建议全程用
echo strlen(serialize($obj));比对,而不是靠肉眼数
触发点选__wakeup还是__destruct?看调用时机
不是所有魔术方法都能稳定触发。__wakeup在反序列化完成瞬间执行,__destruct则要等到脚本结束或对象被unset。实战中:
- 如果目标代码是
$obj = unserialize($input); echo 'done';,__wakeup能立刻触发,__destruct要等echo之后才跑 - 若代码里有
unset($obj)或函数return提前退出,__destruct可能根本没机会执行 - pikachu靶场的unser入口默认走__wakeup,但有些CTF题故意删掉__wakeup,逼你用__destruct+文件包含组合技
- 别忽略__toString:当恶意对象被拼接进字符串(如
echo $obj . 'test';)就会触发,常用于绕过无回显场景
环境与工具避坑清单
本地复现失败,八成是环境细节没对齐:
- PHP版本必须匹配靶场:7.2以下支持
O:+绕过__wakeup,7.4+已废弃;8.1+禁用部分危险函数,system()可能被disable_functions拦截 - 别直接复制别人writeup里的payload:class名长度、属性名、命名空间都可能不同,一个字母差就整个链失效
- 用PHPGGC生成payload后,一定要用
php -r "echo strlen(file_get_contents('payload.txt'));"核对长度,再粘贴到请求中 - 浏览器URL传参时,+号会被转成空格,%00会被截断——改用Burp发raw POST,Content-Type设为application/x-www-form-urlencoded
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











