一、PHP反序列化基础原理
1.1 序列化与反序列化概念
序列化(serialize()):将PHP对象、数组等复杂数据类型,转换为可存储、可传输的字符串格式,本质是对数据结构的格式化编码。
反序列化(unserialize()):将序列化后的字符串,还原为原始的PHP对象或数组。核心风险:用户可控反序列化字符串时,可通过构造恶意字符串,触发类中魔术方法,实现代码执行、文件操作等恶意行为。
1.2 基础序列化格式规则
对象格式:O:类名长度:"类名":成员变量数量:{变量序列化内容}
私有变量:序列化后会携带类名填充,格式为 \0类名\0变量名(不可见空字符 \0)
保护变量:序列化后携带 \0*\0变量名 填充
公有变量:直接序列化变量名,无额外填充字符
1.3 核心魔术方法
反序列化过程中,无需手动调用,满足条件自动触发,是漏洞利用核心:
__wakeup():反序列化完成瞬间优先触发(优先级最高)
__construct():类实例化时触发,反序列化默认不触发
__destruct():对象销毁时触发(脚本结束、对象被回收)
__toString():对象被当作字符串使用时触发
__call():调用对象不存在方法时触发
二、成员变量属性
PHP类中三种变量修饰符,序列化后的格式、长度、解析规则完全不同,是字符逃逸、变量数量绕过的基础前提。
2.1 三种属性变量序列化细节
变量属性 | 修饰符 | 序列化格式 | 实际长度变化 |
公有变量 | public | 直接变量名,如 s:1:"a" | 无额外字符,长度=变量名长度 |
保护变量 | protected | \0*\0变量名 | 固定多2个空字符,长度+2 |
私有变量 | private | \0类名\0变量名 | 空字符+类名长度,长度大幅增加 |
2.2 核心作用
在字符逃逸场景中,私有/保护变量的隐藏填充字符 会改变序列化字符串总长度,配合过滤函数的字符替换、删除逻辑,可实现「字符逃逸」,篡改原有序列化结构。
三、PHP反序列化解析不敏感特性
PHP unserialize() 函数解析字符串时,存在容错不敏感特性,不会严格校验格式完整性,是多种绕过漏洞的核心依据。
3.1 类名大小写不敏感
反序列化解析类名时,忽略大小写。例如类名 Test,序列化字符串中写 test / TEST 均可正常解析,成功实例化对象。
利用场景:WAF拦截特定类名、黑名单过滤大小写敏感字符时,可大小写混淆绕过。
3.2 多余字符自动忽略
unserialize() 只会解析合法序列化结构部分,字符串末尾多余的乱码、无效字符、恶意拼接内容会被直接忽略,不影响反序列化执行。
3.3 变量数量解析不严格
序列化字符串中声明的「成员变量数量」,与实际写入的变量数量不强制匹配:
声明数量 > 实际变量数:正常解析,缺失变量默认赋值为空
声明数量 < 实际变量数:正常解析,多余变量自动忽略
四、字符逃逸(增多逃逸 & 减少逃逸)
字符逃逸核心场景:代码存在过滤/转义函数(如 addslashes、自定义字符替换、黑名单删除),用户输入可控,过滤函数会改变字符数量,导致原有序列化结构错位、逃逸,最终篡改变量值。
核心原理:输入字符经过过滤后,长度发生变化,打破原有序列化字符串的长度匹配规则,实现恶意变量注入。
4.1 减少逃逸(过滤删除字符,长度缩短)
原理
系统过滤函数会删除/替换用户输入的特定字符,导致最终拼接的序列化字符串长度比预期更短,后方合法序列化结构向前错位,恶意构造的变量声明逃逸生效。
场景复现逻辑
1用户输入可控参数,拼接至序列化字符串中;
1后端过滤函数删除输入中的特定字符(如 \、空格、特定字母);
1字符被删除后,预设的长度数值失效,后方正常结构被篡改;
1成功注入恶意变量,覆盖原有变量值。
核心特点
过滤后字符总数 < 原始字符数,吃掉占位长度,实现结构逃逸。常见于 str_replace 删除字符场景。
4.2 增多逃逸(过滤增加字符,长度变长)
原理
系统过滤函数会转义特殊字符(如 addslashes 将 '"\ 转义为 \'\"\\),单个字符变为两个字符,导致序列化字符串长度增加。
场景复现逻辑
1用户输入携带特殊字符,后端自动转义,字符数量增多;
1序列化字符串中「声明的字符长度」是转义前的数值,小于实际转义后长度;
1长度不匹配导致原有闭合结构失效,恶意拼接的序列化代码逃逸执行。
经典案例:addslashes 字符逃逸
用户输入 %00 空字符或 \,被转义为 \\,长度+1,持续拼接后可突破原有长度限制,注入恶意变量。
4.3 增减逃逸核心区别
减少逃逸:过滤删字符 → 总长度变小 → 结构前移 → 篡改后端变量
增多逃逸:过滤加字符 → 总长度变大 → 闭合失效 → 恶意代码溢出
五、变量增多/减少绕过
基于前文解析不敏感(变量数量不严格校验)特性衍生的绕过手法,核心是篡改序列化字符串中的变量数量声明值。
5.1 变量减少绕过
场景:代码校验变量数量不能过多,限制传入参数数量。
绕过思路:序列化字符串中减小变量声明数量,实际保留恶意变量。PHP解析时忽略数量不匹配,正常解析所有变量,绕过数量校验。
5.2 变量增多绕过
场景:代码限制变量数量过少,或固定校验变量个数。
绕过思路:序列化字符串中增大变量声明数量,超出原有正常变量数。PHP自动忽略多余声明,正常解析有效变量,规避校验规则。
总结核心:PHP反序列化不校验变量数量一致性,数量声明值可随意篡改,仅以实际存在的变量为准。
六、__wakeup() 绕过技术
6.1 Wakeup函数原生特性
__wakeup() 优先级最高,反序列化时最先执行,常被开发者用于防护:清空恶意变量、重置参数、过滤危险内容,阻止反序列化漏洞利用。
6.2 经典绕过:变量数量溢出绕过(CVE-2016-7124)
漏洞原理:PHP版本 < 5.6.25 / < 7.0.10 存在解析BUG,当序列化字符串中声明的成员变量数量 大于 类实际定义的成员变量数量时,__wakeup() 函数失效不执行,仅触发 __destruct()。
6.3 绕过步骤
1查看目标类中实际定义的成员变量个数(假设真实变量数为2);
1修改序列化字符串中变量数量字段,改为大于真实值(如改为99);
1反序列化时触发BUG,__wakeup() 跳过不执行;
1直接执行 __destruct() 等后置魔术方法,完成漏洞利用。
6.4 绕过示例对比
正常序列化:O:4:"Test":2:{s:1:"a";s:1:"1";s:1:"b";s:1:"2";}(2个变量,wakeup正常执行)
恶意绕过序列化:O:4:"Test":99:{s:1:"a";s:1:"1";s:1:"b";s:1:"2";}(声明99个变量,wakeup失效)
6.5 拓展:其他Wakeup绕过方式
类名大小写绕过:利用解析大小写不敏感,混淆类名,导致wakeup匹配失效;
字符逃逸绕过:通过字符逃逸篡改类结构,破坏wakeup触发条件;
版本特性绕过:高版本PHP可结合解析冗余字符、变量覆盖绕过wakeup防护逻辑。