PHP反序列化
序列化:对象转换为字节流(数组或字符串等格式);方便对象在内存、文件、数据或网络之间传递;
反序列化:将数组或字符串转换为对象;
反序列化的基本过程
1 |
|

常见触发条件
PHP 中常见的魔术方法:
1 | __call() // 调用不可访问或不存在的方法时被调用 |
反序列化常见起点:
1 | __wakeup // 一定会调用 |
反序列化常跳板:
1 | __get() // 读取不可访问或不存在属性时被调用 |
反序列化常见终点:
1 | __call() // 调用不可访问或不存在的方法时被调用 |
成因:未对用户输入的序列化字符串进行检测,可控反序列化过程,导致代码执行、SQL 注入、目录遍历等;在反序列化的过程中自动触发的某些魔术方法;
通常所需要的利用条件
- 有触发魔术方法;
- 魔术方法有利用类;
- 部分自带类拓展开启;(PHP 版本和配置文件)
基本步骤
生成步骤:
- 把题目代码复制到本地;
- 注释掉与属性无关的内容(方法和没用的代码);
- 对属性赋值;
- 直接对属性赋值(只能赋值字符串);
- 外部赋值(new 完对象后通过对象属性赋值)但只能操作 public 属性;
- 构造方法赋值(eg:在类里的
__construct类中对属性赋值)- 输出 url 编码后的序列化数据:
echo(urlencode(serialize(new DEMO())));- 将序列化数据发送到目标服务器
POP链:POP(面向属性编程)链是指从现有运行环境中寻找一系列的代码或指令调用,然后根据需求构造出一组连续的调用链。
反序列化利用就是要找到合适的 POP 链。其实就是构造一条符合原代码需求的链条,去找到可以控制的属性或方法,从而构造 POP 链达到攻击的目的。
寻找 POP 链的思路:
- 寻找 unserialize() 函数的参数是否可控;
- 寻找反序列化想要执行的目标函数,重点寻找魔术方法(比如
__wakeup()和__destruct());- 一层一层地研究目标在魔术方法中使用的属性和调用的方法,看看其中是否有我们可控的属性和方法;
- 根据我们要控制的属性,构造序列化数据,发起攻击
利用原生类
(初步了解,后续需补充学习完整)
原生自带类导致的 PHP 反序列化漏洞:
https://xz.aliyun.com/news/8792
https://www.anquanke.com/post/id/264823
https://blog.csdn.net/cjdgg/article/details/115314651
https://drun1baby.top/2023/04/11/PHP-%E5%8E%9F%E7%94%9F%E7%B1%BB%E5%AD%A6%E4%B9%A0/
部分绕过方式
绕过 __wakeup
适用版本:php5.0.0 ~ php5.6.25、php7.0.0 ~ php7.0.10;
由于unserialize()后会立即触发__wakeup,可通过修改属性数量的方式来绕过;
1 | // 标准序列化数据 |
快速__destruct
PHP 接收到上面所提到的修改后的不正确的序列化字符串,其可以正常的反序列化,但是由于其不正确性,PHP 会直接触发 __destruct;
某些情况需要利用__destruct来获取 flag ,但其方法执行过于靠后,可能导致在 POC 其之前就会被过滤,此时就需要通过上述修改为不正确字符串来触发;
访问修饰符问题
版本 PHP 7.1+
反序列化对属性类型不敏感,有的属性不是 public ,但是在本地构造时可以改成 public 。
protected 修饰的属性,序列化时,字段名前会加上\00*\00的前缀;(这里的 \00 表示 ASCII 码为 0 的字符,属于不可见字符,因此该字段的长度会比可见字符长度大3。)
private 修饰的属性,序列化时,字段名前会加上\00<declared class name>\00前缀;(这里的<declared class name表示是声明该私有字段的类的类名,而不是被序列化的对象的类名。)
1 |
|
字符串逃逸
当开发者使用先将对象序列化,然后将对象中的字符进行过滤,最后再进行反序列化。此时有可能产生 PHP 反序列化字符逃逸的漏洞。
两种情况:过滤后字符变多/变少;
示例(变多)
1 |
|
1 | O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} |
现在期望的是使 isVIP 的值为 1 ;
1 | ";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} // 现有子串 |
需要再 admin 可控参数处注入目标子串,目标子串长度为 47 ,admin 每变为一次 hacker 会多 1 个字符;
所以这里重复 47 遍 admin ,然后加上逃逸的目标子串,可控变量修改为:
1 | $a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}','123456'); # 替换第一个参数 |
此时的输出结果会变为:
1 | O:4:"user":3:{s:8:"username";s:282:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} |
反序列化后,多余的子串会被抛弃
1 | # 接上面代码~ |
输出:
1 | object(user)#2 (3) { |
示例(变少)
1 | function filter($s){ |
目标子串 47 位,需计算下一个可控变量的字符串长度:
1 | ";s:8:"password";s:6:" |
每次过滤会减少 1 个字符;这里用了 23 个 admin 具体数量需要通过计算测试自己构造;
1 | # 接上面的代码~ |
成功得到结果:
1 | O:4:"user":3:{s:8:"username";s:115:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:5:"isVIP";i:0;} |
主要还是要构造 Payload 来达到注入使序列化后的字符传正常闭合,挤掉后面原本不需要的字符串。
Phar 反序列化
PHP 5.3 开始,引入类似于 JAR 的一种打包文件机制。它可以将多个文件存放在同一个文件中,无需解压,PHP 就可以进行访问并执行内部语句。
Phar 文件结构
1 | Stub //Phar文件头 |
原理:Phar 文件会以序列化的形式存储用户自定义的 元数据(Meta-data),PHP 使用phar_parse_metadata在解析 meta 数据时,会调用php_var_unserialize进行反序列化操作;
Phar属于伪协议,伪协议使用较多的是一些文件操作函数,如fopen()、copy()、file_exists()等,具体如下图,也就是下面的函数如果参数可控可以造成 Phar 反序列化,所以当这些函数接收到伪协议处理到 phar 文件的时候,Meta-data 里的序列化字符串就会被反序列化,实现不使用 unserialize() 函数实现反序列化操作。

利用条件:
- phar 文件(任意后缀都可以)能上传至服务器;
- 存在受影响函数,存在可以利用的魔术方法;
- 文件操作函数的参数可控。
生成 Phar 注意:php.ini 中将 phar.readonly 设置为 off
1 |
|
运行生成 Phar 文件。
存在漏洞代码,通过 file_get_contents 触发 phar 反序列化:
1 |
|
访问该文件,得到 phpinfo() 的回显。
案例:








