news 2026/10/14 4:38:47

PHP反序列化漏洞入门:从源码泄露到__wakeup绕过实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP反序列化漏洞入门:从源码泄露到__wakeup绕过实战

拿到这道题,我先说结论:它几乎是Web方向新手接触PHP反序列化漏洞最顺滑的一道练手题。标题里“PHP”三个字已经剧透了主要考点,但真正做完你会发现,它还顺带把源码泄露排查、魔术方法利用、任意文件读取这几件事串成了一条完整的学习链路。我当年第一次刷到它的时候,连序列化字符串长什么样都没见过,硬是靠着这道题把反序列化的触发逻辑彻底搞懂了。

这篇内容适合以下几类人:刚入门 CTF 但 Web 方向还没找到手感的人;学过 PHP 语法却不知道serialize()和unserialize()能拿来干什么的人;以及准备集中刷题却发现网上 writeup 只给结论不给推导过程的人。我会尽量把每一步“为什么这么做”讲清楚,而不是直接丢一个 payload 让你复制。

1. 开局只有一句话:如何从入口信息顺藤摸瓜

1.1 题目页面最容易被忽略的三个提示

这道题的初始页面非常朴素,通常只有一句带引导性质的话,可能是“你能找到 flag 吗”,也可能是一段 JS 弹窗。很多刚入门的朋友看到这种页面就懵了,觉得“没有输入框、没有按钮,这让我怎么做题”。

我的建议是先把页面当作一个真实站点来测,按固定顺序做三件事:查看页面源码、查看响应头、试探常见敏感路径。页面源码里可能藏着注释掉的后门提示,响应头里可能暴露了服务器软件和 PHP 版本,而敏感路径探测则是整道题的真正入口。这三件事花不了两分钟,却往往能直接决定后续方向。

1.2 备份文件的发现顺序:先浏览器,再脚本

这道题后续的核心源码藏在备份文件里,常见情况是存在www.zip或者index.php.bak。我推荐按下面的顺序来探测,而不是一上来就上爆破字典:

  1. 手工访问robots.txt,看有没有提示性路径。
  2. 直接访问几个高概率备份名:www.zip、index.php.bak、index.php.swp、.index.php.swp、/.git/等。
  3. 如果手工尝试没结果,再写脚本批量探测。

我自己比较习惯先用浏览器把高概率路径点一遍,因为这道题的备份文件名通常是www.zip,属于“一猜就能中”的类型。下载下来解压之后,你会得到整个站点的源码,包括index.php和几个业务类文件,真正的考点这才开始显现。

1.3 一个顺手就能写的敏感路径探测脚本

如果你不想手工点,也可以写一个简单的 Python 脚本,把候选文件名组合出来逐个请求。给一个我常用的雏形:

import requests base = "http://目标地址/" candidates = [ "www.zip", "index.php.bak", "index.php~", ".index.php.swp", "flag.php", "robots.txt" ] for name in candidates: url = base + name try: r = requests.get(url, timeout=5) if r.status_code == 200: print(f"[+] 200 -> {url} (len={len(r.content)})") elif r.status_code == 403: print(f"[*] 403 -> {url}") except Exception as e: print(f"[-] 请求 {url} 出错: {e}")

这种脚本的响应长度字段很有参考价值,比如访问flag.php返回了 200 但内容很短,说明文件存在但被 PHP 解释执行了,页面里看不到 PHP 源码,这种场景后面通常要配合伪协议读取。这属于经验之谈,脚本本身不复杂,重点在于养成“先探测、后审计”的习惯。

2. 源码审计的核心:找到入口参数和两个魔术方法

2.1 从 index.php 定位反序列化触发点

解压下载到的源码后,先打开index.php。通常里面会是一段比较简单的逻辑:引入类文件,从 GET 或者 Cookie 中取值,然后直接把这个值交给unserialize()处理。典型的代码长这样:

<?php include 'class.php'; $select = $_GET['select']; $res = unserialize($select); ?>

看到$_GET['select']直接进unserialize()的那一刻,心里就要有数了:这题的利用点已经锁定,剩下的全部工作就是研究 class 文件里有哪些魔术方法、它们在什么时候被触发、能不能被我们利用。

这里有个小经验:遇到反序列化题目,先把所有“用户可控输入”与unserialize之间的距离看清楚。距离越短,利用成本越低。像这种 GET 参数直连入口的,属于最基础也最典型的情况。

2.2 __wakeup 与 __destruct 的攻防关系

打开class.php之后,你会看到两个关键魔术方法。

__wakeup()会在反序列化开始后、对象被恢复出来之前自动调用。很多题目会在里面加一个判断,比如检查当前用户的身份、检查某个属性值是否合法,如果检查不通过就直接退出,不让你继续往下走。

__destruct()则在对象被销毁时自动调用,通常是在脚本结束时。很多题目会把真正敏感的操作放在这里,比如读取文件、拼接路径、输出内容。

这两者的关系可以理解成:__wakeup()是赛题作者设置的“安检门”,__destruct()是放“重要物品”的地方。我们要做的事,就是想办法让“重要物品”被拿出来,但又不让“安检门”正常工作。

拿这道题的经典场景来说,源码里通常会在__wakeup()中判断某个属性是否为特定值,如果不是就输出提示并结束脚本。而__destruct()中则可能会读取一个属性指定的文件路径,并把文件内容输出出来。如果我们能构造一个特殊对象,让__destruct()读取flag.php,同时绕过__wakeup()的拦截,题目就解完了。

2.3 为什么这个题目对新手特别友好

我觉得这道题设计得好的地方在于,它把反序列化的几个核心概念分得非常清楚:触发点(unserialize入口)、检查逻辑(__wakeup)、利用逻辑(__destruct文件读取),三者独立清晰,没有多余的干扰项。你只要能理解序列化字符串的结构,再配合一个经典绕过技巧,就能打通整条链路。

它不会像某些进阶题那样把 POP 链叠得很长,也不涉及复杂的作用域绕过。所以我很推荐做题顺序是这样的:先把它完全吃透,把序列化字符串每个字节的含义都弄明白,再去看更复杂的 POP 链题目,会感觉后面的东西都是在这道题的基础上做加法。

3. 绕过 __wakeup() 的经典操作:利用属性计数不一致

3.1 先看懂序列化字符串的结构

要说绕过,脑子里第一步要建立起序列化字符串的基本结构。拿一个最简单的类来说:

class Test { public $name = "flag.php"; }

用serialize()生成出来的字符串长这样:

O:4:"Test":1:{s:4:"name";s:8:"flag.php";}

拆开看就是:O表示对象,4是类名长度,Test是类名,1是属性数量,花括号里依次是属性名和属性值。理解这个格式非常重要,因为接下来的绕过就是在这个格式上动手术。

3.2 CVE-2016-7124:把属性个数改大就能跳过检查

这个绕过技巧对应的是一个公开的 PHP 反序列化漏洞,编号是 CVE-2016-7124。核心原理是:当反序列化字符串中声明的属性个数,大于对象实际具有的属性个数时,PHP 会跳过__wakeup()魔术方法的执行。

回到这道题,假设class.php里的类只有一个属性,那正常生成的序列化字符串中属性个数就是1。我只需要手动把它改成2、3甚至更大,PHP 在处理时就会因为读不满声明的属性,直接跳过__wakeup(),进而顺利走到__destruct()。

用我自己的话来说,这个漏洞的本质是:PHP 在反序列化时,对“声明了多少属性”和“实际上有多少属性”这两件事没有做严格的一致性校验,给了攻击者一个利用空间。

3.3 版本限制和我的实测记录

这个绕过并不是在所有 PHP 版本上都好使。受影响的版本范围是 PHP 5.6.25 以下,以及 PHP 7.0.10 以下的版本。我本地搭环境练习的时候,用的是 PHP 5.6.24,实测可以稳定绕过;后来换到 PHP 7.4,同样的 payload 就不起作用了,这会直接导致__wakeup()照常执行并中断脚本。

所以做题前有一个建议:先想办法确定目标环境的大致 PHP 版本。常见的判断方法有:看响应头里的X-Powered-By、根据报错信息推测、访问phpinfo.php(虽然多数题目不会留),或者根据题目提示猜。如果确定是低版本环境,那这个绕过就是首选方案。

# 本地调试时,可以用 Docker 快速起一个低版本 PHP 环境 docker run -d -p 8080:80 --name php56 php:5.6-apache

把题目源码放进容器对应目录,再访问http://127.0.0.1:8080就能开始调试。这个环境比较接近早期 CTF 题的常见配置,对还原这道题的利用场景很有帮助。

3.4 绕过手段的拓展:不是只有属性计数这一招

属性计数绕过最经典,但我也提醒自己:__wakeup()不是只有这一种方式能绕。比如在某些场景下可以尝试让对象在反序列化之前就被销毁,或者利用异常打断__wakeup()的执行流程,或者在 PHP 7.4 以上版本利用__unserialize()的特性差异来换一种思路。

但回归到这道题,属性计数法就是最直接、最稳的方案。先把这一招练到滚瓜烂熟,再去看别的奇技淫巧会容易得多。一次搞定一个点,效果好。

4. 完整利用链实测:从构造 payload 到输出 flag

4.1 第一步:本地生成序列化字符串

既然要手动改属性个数,最省事的方式是先在本地用 PHP 生成原始的序列化字符串,再手动修改。我通常的做法是写一个临时 PHP 脚本:

<?php class Name { public $name = "flag.php"; } $a = new Name(); echo serialize($a); ?>

执行之后会输出:

O:4:"Name":1:{s:4:"name";s:8:"flag.php";}

接下来把属性个数从1改成2,得到:

O:4:"Name":2:{s:4:"name";s:8:"flag.php";}

这里需要注意,修改时必须保证类名、属性名、属性值的格式完全正确,任何一个长度没对上,PHP 反序列化就会失败。新手最容易在这个环节把s:4:"name"的长度写错,一旦长度不对,整个字符串就是废的。

4.2 第二步:利用 php://filter 读取 PHP 文件源码

这道题里 flag 通常放在flag.php中,而且很可能写在 PHP 注释里。如果直接用已经修改好的序列化字符串让__destruct()去读取flag.php,返回的只会是空内容或者一句“什么都没有”,因为 PHP 会把flag.php当脚本执行,注释里的内容不会出现在输出中。

解决办法是使用 PHP 流过滤器,以php://filter包裹文件路径,让它输出文件的原始内容。最终传入name属性的值应该是这样:

php://filter/read=convert.base64-encode/resource=flag.php

这样读到的会是 base64 编码后的源码,我再手动解码就能看到注释里的 flag。这里有一点经验:凡是发现“文件确实存在、但页面里看不到内容”的情况,优先考虑用 filter 读源码,这是 CTF 里的高频操作。

4.3 第三步:完整的 GET 请求长什么样

经过 URL 编码后,完整的请求会像这样:

http://目标站点/index.php?select=O%3A4%3A%22Name%22%3A2%3A%7Bs%3A4%3A%22name%22%3Bs%3A61%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D

当目标 PHP 环境执行这个请求时,流程是:反序列化字符串声明属性个数为 2,实际对象只有 1 个属性,触发 CVE-2016-7124 绕过__wakeup(),随后__destruct()执行,用php://filter读取flag.php的 base64 编码内容并输出到页面。

我实测时习惯先用浏览器直接访问,看到输出是一串 base64 字符,再丢到解码工具里,或者用命令一行的方式解:

echo "PD9waHAg..." | base64 -d

解码之后就能看到完整的 PHP 源码,flag 一般在里面躺得好好的。

4.4 容易踩的坑:请求失败时按序排查

自己实操的时候,最常见的几个问题我总结一下:

  1. 完全没输出:说明unserialize失败,检查序列化字符串的语法和长度计数,尤其是属性名长度和类名长度。
  2. 输出了内容但 base64 解码是乱码:说明 filter 路径写错了,检查convert.base64-encode这段的拼写。
  3. 输出了提示但没出现 flag 相关字符:说明__wakeup()还是被触发了,确认目标 PHP 版本是不是在受影响范围内,以及属性个数是不是真的改成大于实际值了。
  4. 什么都正常但没有读取权限:这种情况在这道题里少见,真要碰到再做权限判断,不过 CTF 靶机一般不会限制这种读取。

5. 用一道题拆出三类考点:把经验迁移到后续刷题

5.1 考点一:敏感文件探测与源码审计习惯

表面上看这题考的是反序列化,但真正卡住很多新手的其实是第一步“怎么拿到源码”。后续做题你会发现,很多题目都存在源码泄露问题:.git目录泄露、.svn泄露、DS_Store泄露、各种编辑器备份文件残留。把这些敏感文件的命名规则混个脸熟,刷题的效率能提升一大截。

我还建议养成本地审计源码的习惯,把index.php、业务类文件、配置文件的读取顺序固定下来,先看入口,再看类定义,最后看内部逻辑。这样碰到复杂题目时不会手忙脚乱。

5.2 考点二:反序列化漏洞的识别与利用链推导

识别反序列化入口有几个固定信号:看到unserialize($_GET...)、unserialize($_POST...)、unserialize(base64_decode(...))这类模式,基本就是考点。后面的工作就是分析输入可控的对象结构,找到可利用的魔术方法,然后构造一条能触发敏感操作的利用链。

从这道题学到的核心思维是:不要只想着把对象恢复出来,要想办法让对象在被构造、被销毁、被访问属性的过程中执行我们期望的操作。魔术方法很多,不同方法有不同的触发时机,做题时要把每个方法的触发条件往源码里对应。

5.3 考点三:任意文件读取与流过滤器取值

php://filter是 Web 方向非常高频的读取技巧,除了读 PHP 源码,它还能在部分场景中绕过一些简单的过滤规则。把php://filter的各种读法练熟之后,再看data://、phar://这类协议,会触类旁通。

5.4 防守视角的反推:这个漏洞该怎么防

做 CTF 题不只是为了解题,我也会习惯性地想一下“这段代码在真实站点里怎么避免被利用”。对反序列化漏洞来说,最有效的防护原则很朴素:永远不要直接反序列化用户可控的数据。如果确实需要传递结构化数据,优先考虑 JSON 这类格式;如果一定要用,也要做好白名单校验和对象类型限制。

源码备份文件则是另一个真实世界的高频问题:备份文件不能放在 Web 根目录下,或者至少要用无规律命名并限制访问。很多站点被拖源码,都是因为www.zip、backup.zip这类文件直接暴露在公网。这道题的入口本质上就是在模拟这种真实事故。

做完这道题,我自己的体会是:它像一把钥匙,把“看到unserialize之后该往哪想”这件事给彻底带入门了。最后再分享一个练习小技巧:不要只看 writeup,本地搭建一个同样版本的环境,把序列化字符串的每个字节都手动改一遍,观察不同改动导致的不同报错和绕过效果。这个动作看起来枯燥,但比连着刷十道题都管用,因为只有亲眼看到__wakeup()什么时候被跳过、什么时候没被跳过,才会真正理解属性个数绕过的底层逻辑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 4:37:37

Unity应用加密实战:三层防御体系与工程化落地指南

1. 项目概述&#xff1a;为什么Unity App加密不是“加个壳”就完事了“Unity app加密”这五个字&#xff0c;最近在开发者社区里出现频率陡增——不是因为技术突飞猛进&#xff0c;而是因为踩坑的人实在太多了。我接触过不下二十个团队&#xff0c;有刚毕业的独立开发者&#x…

作者头像 李华
网站建设 2026/10/14 4:37:36

安卓、鸿蒙、Fuchsia架构对比:三条操作系统的不同路线

做跨端开发这几年&#xff0c;我被人问过最多的问题就是&#xff1a;鸿蒙、Fuchsia、安卓&#xff0c;到底哪个更值得跟进&#xff1f;说实话&#xff0c;这个问题本身就有陷阱。你要是把三个系统的技术规格拉出来逐项对比&#xff0c;会发现它们连“要解决什么问题”都不一样。…

作者头像 李华
网站建设 2026/10/14 4:36:33

Windows下鸿蒙跨平台开发环境配置与编译脚本实战

如果你刚开始搞鸿蒙跨平台开发&#xff0c;第一道坎往往不是代码本身编译不过&#xff0c;而是开发环境怎么配都报错。我自己在 Windows 上折腾 KuiklyUI 这套跨平台 UI 框架时&#xff0c;光环境变量就反复配了好几轮&#xff0c;后来又发现编译脚本里的调用顺序、缓存清理、S…

作者头像 李华
网站建设 2026/10/14 4:36:01

CentOS Stream上Docker Compose部署WordPress

忙完一个容器化改造项目&#xff0c;我顺手把阿里云 CentOS Stream 上构建 WordPress 的完整过程整理出来。这次不是演示环境的玩具方案&#xff0c;是实际部署并运行了一段日子的组合&#xff1a;Docker Compose 管理四个容器&#xff0c;Nginx 做 Web 入口&#xff0c;PHP 单…

作者头像 李华
网站建设 2026/10/14 4:35:59

多阶段构建实战:从1.6GB到30MB的Docker镜像优化指南

我记得第一次认真面对镜像臃肿这件事&#xff0c;是因为一套前后端服务的生产部署&#xff1a;前端是 Vue 打包出来的静态资源&#xff0c;后端是 Go 写的接口服务&#xff0c;按我当时的“直觉”写了一个 Dockerfile&#xff0c;把源码、依赖、构建工具一股脑全塞进镜像里&…

作者头像 李华
网站建设 2026/10/14 4:35:59

批量转双层PDF:OCRmyPDF+Tesseract多语言识别实战指南

上个月我接了一个整理扫描件的活儿&#xff0c;三千多页混合语种材料&#xff0c;中文、英文、繁体&#xff0c;还夹着几页日文表格。靠肉眼翻&#xff0c;三天都翻不完&#xff0c;更别提要从里面快速定位某一条内容。这个场景下&#xff0c;唯一靠谱的办法就是做双层PDF——给…

作者头像 李华