news 2026/10/13 10:51:36

PHP反序列化绕过实战:__wakeup与正则过滤的攻防博弈

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP反序列化绕过实战:__wakeup与正则过滤的攻防博弈

我第一次在攻防世界遇到web_php_unserialize这道题时,说实话挺懵的。标题里明晃晃写着"unserialize",知道是 PHP 反序列化,但打开环境一看源码就卡住了:类里有个__wakeup方法直接exit,默认的序列化字符串还会被正则拦下来。我试了几次,要么页面弹bad requests,要么直接回hacker,完全找不到突破口。后来我耐着性子把序列化格式手工拆了一遍,又单独验证两个绕过点,才真正想明白:这道题一点都不难,难的是没人把"为什么"讲透。这篇文章就当一次胎教级复盘,从读源码开始,把__wakeup绕过、正则绕过、payload 构造、本地复现全部说清楚,保证你能完整跑通。

1. 读题:先看看这道题到底想让我们干什么

1.1 题目源码还原

攻防世界这道题的源码,网上流传的版本大同小异,核心下来是这样的:

<?php class xctf{ public $flag = '111'; public function __wakeup(){ exit('bad requests'); } } $temp = isset($_GET['user']) ? $_GET['user'] : 'a:2:{s:5:"xctf";O:4:"xctf":1:{s:4:"flag";s:3:"111";}}'; if(preg_match('/[oc]:\d+:/i', $temp)) { die('hacker'); } else { $temp1 = unserialize($temp); echo $temp1['xctf']->flag; }

有的环境里isset写法会被替换成 PHP 7 的??,不影响后面的分析。第一眼看到这段代码,别急着找工具乱试,先把结构拆出来:

  • 定义了一个xctf类,里面只有一个 public 属性$flag,默认值是'111'。
  • __wakeup()是魔术方法,只要反序列化出来的是这个类的实例,它就自动执行,执行内容就是exit('bad requests')。
  • 页面接收user参数;如果没传,就用一个默认的序列化字符串。
  • 反序列化之前,先过一道正则/[oc]:\d+:/i,匹配到就输出hacker并结束。
  • 最后,unserialize的结果被当成数组,取['xctf']键对应的对象,再打印它的->flag。

所以目标非常明确:让unserialize成功执行、不触发__wakeup,最后让echo把flag属性的值打出来。这一步想清楚,后面所有构造都围绕它展开。

1.2 三个关键结论

读到这里可以顺手总结出三个结论。

第一,入口参数是GET参数user。我们不需要依赖平台上任何表单或按钮,直接构造请求就行,用浏览器、Burp 或者 Python 脚本都可以。

第二,最大的拦路虎是__wakeup。因为只要反序列化对象成功,它就会执行然后退出,所以我们必须让这个对象"不正常醒来"。怎么不正常醒?这是后面第 3 章的重点。

第三,正则/[oc]:\d+:/i专门盯着序列化串里的对象和类标记。典型的O:4:这种写法一进去就被打回。所以 payload 里还得绕过这个正则。

再注意默认的序列化字符串:

a:2:{s:5:"xctf";O:4:"xctf":1:{s:4:"flag";s:3:"111";}}

它最外层是一个数组,数组中声明了 2 个元素,但里面实际上只有一个键值对:键是字符串xctf,值是xctf对象。这个a:2在老版本 PHP 里能被容忍,但新版本对数组长度不匹配会直接报错。这也是为什么网上有些 wp 直接抄默认值结构,抄到本地新环境就失效。

关于flag属性我还要多说一句:在线环境的题目里,类中的$flag初始值往往只是占位符,平台运行时会把它替换成这道题真正的 flag 串。所以我们不是去猜 flag 值,而是要让程序执行到echo,把类里已经写好的真实 flag 打出来。这也是我后面推荐某些 payload 变体的原因——你如果在序列化串里把flag属性写死,输出就会是你写死的那个值,而不是题目真正的 flag。

2. 反序列化基础:不懂这些,payload根本写不出来

2.1 序列化格式速记表

在动手构造前,先把 PHP 序列化格式过一遍。serialize()把一个对象变成字符串,unserialize()再把字符串还原成对象。这个字符串是纯文本,每种数据类型都有固定表达方式。

类型序列化格式示例
布尔b:值b:1
整数i:值i:123
字符串s:长度:"内容"s:5:"admin"
数组a:元素个数:{键;值;...}a:1:{s:1:"a";i:1;}
对象O:类名长度:"类名":属性个数:{属性}O:4:"xctf":1:{...}
引用R:整数 或 r:整数较少用到,了解即可

对象格式里,每个属性的结构是"属性名 + 属性值",比如:

O:4:"xctf":1:{s:4:"flag";s:3:"111";}

意思是:类名xctf长度是 4,对象里有 1 个属性,属性名是字符串flag(长度 4),属性值是字符串111(长度 3)。

很多人第一次手工构造就栽在长度上。比如把s:4:"flag"里的 4 写成 3,PHP 解析时就会错位,轻则解析失败,重则把后面的内容读成乱码。所以后面我给的 payload 里,凡是字符串都要反复核对长度。

2.2 __wakeup到底在多早执行

__wakeup是 PHP 反序列化对象的魔术方法之一。它的触发时机是:unserialize()解析到对象格式时,先把对象实例化出来,填充完属性,然后立刻调用__wakeup()。也就是说,不管你怎么构造属性,只要这个类有__wakeup,它就会在这个时机执行。

拿本题来说,__wakeup里直接exit,于是整个脚本终止,后面的echo $temp1['xctf']->flag永远不会执行。对于这种"摆明了不让你反序列化"的守卫逻辑,CTF 里最常见的办法就是利用它的一个历史绕过。

注意__wakeup和__construct不一样。普通new对象时走的是构造函数,而unserialize()创建对象时不一定走构造函数,它更常调用__wakeup。可以理解成:__wakeup是对象从字符串里"醒过来"时的初始化动作。

2.3 public属性的序列化细节

public属性在序列化时就是普通字符串键值对,没有任何额外修饰。比如:

class User { public $name = 'admin'; }

序列化结果是:

O:4:"User":1:{s:4:"name";s:5:"admin";}

如果属性是protected,键名会变成\0*\0name这种带空字符的形式,长度和肉眼看到的完全不同;如果是private,键名会带类名前缀。很多新手在这一步吃亏,是因为直接拿public的写法去套protected属性,长度填错了还不知道。

本题的$flag是public,所以我们只需要处理普通的键值对形式。这省了不少事。

3. 绕过点拆解:__wakeup和正则一个都不能少

3.1 CVE-2016-7124:为什么把属性个数改大就能跳过__wakeup

这道题的关键漏洞是 CVE-2016-7124,官方描述是:在某些 PHP 版本中,当反序列化时对象属性个数大于真实属性个数,__wakeup()不会被执行。

受影响的版本主要是 PHP 5.6.25 之前、PHP 7.0.10 之前的版本。修复之后,这种"数量伪造"就不再生效了。攻防世界的这道老题在线环境一般用的还是存在漏洞的 PHP 版本,所以我们可以放心利用。

为什么数量不一致就能跳过__wakeup?原理可以这样理解:PHP 反序列化对象时,会先读取序列化串里声称的属性个数,然后按这个数量去逐个读属性。正常情况下,序列化串里写几个属性,就有几个属性。如果解析器读到对象声明的属性个数位置时,发现后面已经遇到右大括号}结束,它就会提前停止。这个"提前停止"的行为使得后面的__wakeup调用被跳过了。

举个例子。下面的对象声明有 2 个属性,但实际只写了 1 个属性:

O:4:"xctf":2:{s:4:"flag";s:3:"111";}

解析流程是:读类名xctf,读属性个数 2,开始读属性;读到第一个属性flag=111,再准备读第二个属性时,发现字符串已经结束了,于是停止属性解析。这种情况下,PHP 在某些版本里就不会继续走到__wakeup,反而直接把这个对象返回。

这个技巧对这个题的启示很直接:把对象属性个数从 1 改成 2,也就是让声明个数大于实际个数,就能绕过__wakeup里的exit。

3.2 正则/[oc]:\d+:/i与O:+4的博弈

绕过__wakeup之后,还有一道关卡:preg_match('/[oc]:\d+:/i', $temp)。

正则拆开看:

  • [oc]:匹配字母o或c,不区分大小写;
  • ::匹配冒号;
  • \d+:匹配一个或多个数字;
  • ::再匹配一个冒号。

它专门匹配O:4:这种对象序列化片段。为什么还要管c?因为 PHP 里还有一种类序列化格式是C:类名长度:"类名":,通常用于实现了Serializable接口的类。

最常见的绕过方法是在数字前面加一个+,把O:4:写成O:+4:。原因也很简单:正则里的\d+要求数字紧跟冒号,而+不是数字,所以O:+4:不匹配正则。但 PHP 的unserialize在解析数字时,本身就把+4当作有符号整数 4,完全能正常读出来。

我见过有人试着用O:04:绕过,这个思路不行,因为04同样是数字,正则照样匹配。还有人试大小写,更没用,正则带了/i。所以在本题里,最干净利落的绕过方式就是O:+4:。

3.3 两层组合后的构造思路

现在思路很清楚,我们需要构造一个xctf对象,同时做到三件事:

  1. 序列化格式能被 PHP 正常解析;
  2. 属性个数声明大于实际属性个数,骗过__wakeup;
  3. 用O:+4:骗过正则。

一开始可以先写标准格式:

O:4:"xctf":2:{s:4:"flag";s:3:"111";}

再改成:

O:+4:"xctf":2:{s:4:"flag";s:3:"111";}

这个版本是目前网上绝大多数 wp 的写法。但我实测中发现它有个隐藏的坑:它把flag属性直接写成111并放进序列化串里,反序列化后flag属性会被覆盖成111。如果在线环境里题目真正的 flag 是通过类属性默认值注入的,你最终输出会是111,而不是 flag。这一点很少有人在 wp 里说明。

更稳妥的做法是:把属性名写成一个类里不存在的名字,比如fla,让它去当"挡箭牌"占用属性个数,真正的flag属性保留类默认值。具体的 payload 我放在下一章一步步构造。

4. 手把手构造payload:推荐带假属性名的版本

4.1 先写对象部分

我们最终要传给user的是一段可被unserialize识别的字符串。先写内层对象:

O:+4:"xctf":2:{s:4:"fla";s:3:"111";}

逐段解释:

  • O::表示这是一个对象;
  • +4:类名长度为 4,+用于绕过正则;
  • "xctf":类名;
  • 2:声明属性个数为 2,大于实际属性个数 1,用于绕过__wakeup;
  • {s:4:"fla";s:3:"111";}:实际只写了一个属性,属性名叫fla,属性值是111。

这里为什么用fla而不是flag?因为xctf类里没有fla这个属性,PHP 反序列化时会把它当成动态属性创建出来,类里真正的flag属性不会被覆盖。而2大于1,满足 CVE-2016-7124 的绕过条件。这样既绕过了__wakeup,又保住了真实flag属性。

如果你只是想先在本地验证流程,把fla换成flag也可以,只是输出会变成111,你看不到真实 flag。两种写法建议都试一遍,你会对属性覆盖有更直观的理解。

4.2 包一层数组

题目最后一行是:

echo $temp1['xctf']->flag;

$temp1['xctf']这种写法说明$temp1必须是数组,数组里xctf键对应一个对象。所以内层对象外面还要包一层数组:

a:1:{s:5:"xctf";O:+4:"xctf":2:{s:4:"fla";s:3:"111";}}

结构拆开:

  • a:1:数组,1 个键值对;
  • s:5:"xctf":第一个键,字符串xctf,长度 5;
  • O:+4:"xctf":...:第一个值,也就是xctf对象;
  • }:数组结束。

这里我用a:1而不是a:2。原因有两个:第一,符合数组真实元素个数,最规范;第二,新版 PHP 对数组长度不匹配比较敏感,a:2在 PHP 7 以上可能会解析报错。老环境下a:2也许能容忍,但没有必要去赌。

4.3 URL编码与提交

user参数是放在 URL 里的,所以必须做 URL 编码。尤其是+,在 URL 里会被当成空格,必须编码成%2B。双引号、冒号、花括号这些字符最好也一起编码,避免被浏览器或 WAF 处理掉。

完整请求可以是:

GET /?user=a%3A1%3A%7Bs%3A5%3A%22xctf%22%3BO%3A%2B4%3A%22xctf%22%3A2%3A%7Bs%3A4%3A%22fla%22%3Bs%3A3%3A%22111%22%3B%7D%7D

如果你用 Burp 的 Repeater,直接把这个 URL 编码后的字符串粘到请求头后面就行。浏览器地址栏也可以访问,但注意有些浏览器会自作聪明地解码或转义引号,能用 Burp 就用 Burp。

4.4 常见的翻车现场

我总结了几个容易踩的坑:

  • +没编码成%2B。这是最多的翻车原因,因为 URL 里+表示空格,PHP 拿到的参数会变成O: 4:,反序列化直接失败。
  • 属性个数写成了1。那样等于没绕过__wakeup,页面会回bad requests。
  • 外层写成了对象。如果user传的是O:+4:"xctf":...而不是数组,$temp1['xctf']这一步会报错,因为对象不能像数组一样按下标访问。
  • 字符串长度写错。比如s:4:"fla",fla明明只有 3 个字符,长度必须写 3。如果长度和实际不符,PHP 解析会错位。
  • 把fla写成flag后,输出变成111,误以为题目有问题。其实是你覆盖了真实 flag 属性。

如果上面都没问题,但页面还是没有输出,那可能和 PHP 版本有关,我建议直接跳到第 5 章,在本地用老版本复现一下。

5. 本地环境验证:用PHP 5.6老版本跑一遍

5.1 为什么是PHP 5.6.25之前

CVE-2016-7124 的修复版本分界大致在 PHP 5.6.25 和 PHP 7.0.10。如果你现在系统里装的是 PHP 7.4 或 PHP 8.x,直接拿这个 payload 去测,__wakeup还是会被正常调用,页面还是会退出。这不是 payload 写错,而是漏洞已经修掉了。

本地验证时不要偷懒,直接在官网下载老版本源码自己编译,或者用现成集成环境切到 PHP 5.5 / 5.6 早期版本。注意不是所有"PHP 5.6"都有漏洞,5.6.25 之后的版本已经修复。所以版本号一定要看仔细。

5.2 快速起一个测试环境

最简单的办法是用 Docker 跑一个老版本 PHP。比如:

docker run -d -p 8080:80 --name php56 php:5.5-apache

然后把题目源码放到容器里的站点目录:

docker cp index.php php56:/var/www/html/

如果官方php:5.5-apache镜像版本不在漏洞范围内,可以找更早的 tag。也可以直接用 phpstudy 这类集成环境,在 Windows 上切换 PHP 版本非常方便。

本地源码可以简化成下面这样,只保留核心逻辑:

<?php class xctf{ public $flag = '111'; public function __wakeup(){ exit('bad requests'); } } $temp = isset($_GET['user']) ? $_GET['user'] : ''; if(preg_match('/[oc]:\d+:/i', $temp)) { die('hacker'); } else { $temp1 = unserialize($temp); echo $temp1['xctf']->flag; }

5.3 命令行和浏览器双验证

命令行验证最直接,把序列化字符串放在php -r里跑:

php -r 'class xctf{public $flag="111";public function __wakeup(){exit("bad requests");}} $a=unserialize("O:+4:\"xctf\":2:{s:4:\"fla\";s:3:\"111\";}"); echo $a->flag;'

如果你用的是有漏洞的 PHP 版本,这条命令会输出111。如果没有漏洞,这里不会输出任何东西,因为exit会先执行。

浏览器验证就是把题目源码放到 Web 目录后,访问:

http://127.0.0.1:8080/?user=a%3A1%3A%7Bs%3A5%3A%22xctf%22%3BO%3A%2B4%3A%22xctf%22%3A2%3A%7Bs%3A4%3A%22fla%22%3Bs%3A3%3A%22111%22%3B%7D%7D

正常情况下页面会输出111,因为没有平台注入真实 flag。在线题目环境里,这个位置输出的就是真 flag。

5.4 对照组实验

你可以再跑一个对照组,确认__wakeup确实在执行:

php -r 'class xctf{public $flag="111";public function __wakeup(){exit("bad requests");}} $a=unserialize("O:+4:\"xctf\":1:{s:4:\"fla\";s:3:\"111\";}"); echo $a->flag;'

这串里属性个数写的是1,和实际属性个数一致,所以__wakeup会被触发,页面直接输出bad requests。两个命令一对比,你就能直观感受到"属性个数多写一个"到底改变了什么。

如果本地没有老版本 PHP,还可以用在线 PHP 沙盒去验证。不过要注意,很多在线沙盒跑的也是新版本 PHP,漏洞可能已经修复。这时候就不能用__wakeup绕过作为唯一验证点,可以只看正则绕过是否生效。

6. 从一道题到一类题:PHP反序列化的通用解题模型

6.1 四个固定步骤

刷多了你会发现,PHP 反序列化的题目基本都能用一套固定流程解:

第一步,找到unserialize入口。题目里出现unserialize($_GET['xxx'])或unserialize($_POST['xxx'])就是信号。输入的点通常就在反序列化函数参数上。

第二步,找魔术方法。__wakeup、__destruct、__toString、__call这些都可能被反序列化触发。本题是__wakeup直接exit,所以重点在绕过它。

第三步,想清楚目标数据流。本题最终是打印$temp1['xctf']->flag,所以反序列化的结果必须是一个数组、数组里有一个xctf对象。

第四步,处理过滤。正则、关键字替换、长度限制都要逐个绕过。本题的正则过滤比较典型,属于"一眼能看出来在过滤对象标记"的题型。

6.2 如果环境没有正则限制

这道题如果再简单一点,把正则那行去掉,payload 直接就是:

a:1:{s:5:"xctf";O:4:"xctf":2:{s:4:"fla";s:3:"111";}}

连 URL 编码也能少操心很多——只要把非 URL 字符编码一下就行。实际上很多类似的入门题目就是这样,只考__wakeup绕过,不考正则。所以建议先把这套不带正则的 payload 原理吃透,再去看O:+4这种花活。

正则过滤本身也是一个信号:出题人知道这个漏洞,所以加了层障碍。遇到这种题,先判断能不能用+号、空格、十六进制、大小写变化绕过,再考虑更复杂的编码技巧。

6.3 个人经验:别背payload,手写序列化格式

最后分享一个我自己的体会。很多新手遇到反序列化题,第一反应是去网上搜现成的 payload,拿过来改吧改吧就提交。这样能过题,但换个环境、换个类名、换个属性结构,立刻就懵了。我建议花一个小时,自己在本地把serialize()的结果打印出来,对着格式表拆解一遍,再手工构造一个最简单的对象,用unserialize()验证。这个过程走完,后面的反序列化题基本都能自己推出来。

至于这道题本身,真正有价值的不是记住O:+4:"xctf":2:{...}这几个字符,而是理解:序列化格式里的长度、个数、符号,每一样微小的变化都可能改变 PHP 的解析行为。把这些行为摸透了,再看到新的过滤规则,你也能很快找到对应的绕过思路。把这个过程在本地跑通一遍,从手写序列化串到拿到输出,比看十篇 wp 都管用。

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

DEAP数据集 2026最新Transformer算法 DEAP 数据集 下载

DEAP数据集下载&#xff1a;https://gitee.com/CodeStoreHub/deap-fat Transformer算法下载&#xff1a;https://gitee.com/CodeStoreHub/deap-fat 本算法将傅里叶启发的周期性表示与可学习的通道邻接关系结合&#xff0c;在 Transformer 中同时建模样本相关性与共享结构&…

作者头像 李华
网站建设 2026/10/13 10:47:18

WSL2驱动更新后CUDA与Anaconda环境失效?完整修复指南

更新Windows侧NVIDIA驱动之后WSL里的CUDA直接罢工、Anaconda环境仿佛人间蒸发&#xff0c;这套组合拳打下来&#xff0c;是很多深度学习开发者的噩梦。我在真实项目里也撞过这个坎&#xff0c;当时正在跑的一个模拟训练脚本直接瘫掉&#xff0c;先是Python报 CUDA initializat…

作者头像 李华
网站建设 2026/10/13 10:47:12

2026年AI Agent全栈开发实战:从需求拆解到自动化验证与避坑指南

很多读者最近都在问同一个问题&#xff1a;2026 年了&#xff0c;大家都在用 AI Agent 构建全栈应用&#xff0c;那还需要从零手写一个项目吗&#xff1f;我的看法很直接&#xff1a;需要&#xff0c;但“需要”的方式变了。以前你要靠手写把每一步跑通&#xff0c;现在你要靠 …

作者头像 李华
网站建设 2026/10/13 10:46:56

FUXA开源Web组态软件实战:设备数据采集与监控大屏搭建

很多做设备监控的人&#xff0c;第一次听到FUXA这个开源可视化组态软件时&#xff0c;心里大概都会嘀咕&#xff1a;一个靠浏览器跑起来的组态界面&#xff0c;真能在现场扛得住吗&#xff1f;我最初也是这个反应&#xff0c;但后来真拿它做了一个设备数据采集项目&#xff0c;…

作者头像 李华
网站建设 2026/10/13 10:46:55

SpringBoot代驾管理系统实战:从需求拆解到并发抢单完整落地

一到毕业设计季&#xff0c;我身边就有不少同学来问选题。说句实话&#xff0c;管理系统类项目做得好的不多&#xff0c;大部分最后都变成了“登录增删改查”四件套&#xff0c;答辩时被问一句“你的难点在哪”就卡住了。不过基于SpringBoot的代驾管理系统是个例外&#xff0c;…

作者头像 李华
网站建设 2026/10/13 10:46:54

Flutter鸿蒙适配实战:跨平台演讲稿生成器APP开发全解析

1. 需求拆解与技术选型&#xff1a;这个APP到底做什么 1.1 演讲稿生成器的核心功能画像 事情得从我们项目组接到的一个需求说起。当时几个部门都在准备季度汇报&#xff0c;领导层希望我们做一个内部工具&#xff0c;让员工输入演讲主题、目标时长、场合类型&#xff0c;就能自…

作者头像 李华