PHP反序列化

PHP反序列化
Takake以下是 PHP 中序列化(Serialization)与反序列化(Unserialization)的基础知识总结:
1. 序列化(Serialization)
- 定义:将数据结构或对象转换为可存储或传输的字符串格式(二进制安全)。
- 函数:
serialize($data)
- 支持类型:基本类型(
int
、string
、bool
、float
、null
)、数组、对象。 - 对象序列化:
- 序列化对象时会保存类的名称、属性和值(不包括方法)。
- 如果类定义了魔术方法
__sleep()
,序列化时会自动调用该方法,可以指定需要序列化的属性列表。
1 | class User { |
2. 反序列化(Unserialization)
- 定义:将序列化的字符串还原为原始数据结构或对象。
- 函数:
unserialize($serializedStr)
- 对象反序列化:
- 反序列化时会自动调用类的构造函数(
__construct()
)不触发,但会调用__wakeup()
方法(如果存在)。 - 如果类未定义,对象会转为
__PHP_Incomplete_Class
(见之前讨论)。
- 反序列化时会自动调用类的构造函数(
示例:
1 | $serialized = 'O:4:"User":1:{s:4:"name";s:5:"Alice";}'; |
3. 魔术方法
反序列化的底层逻辑是:① 创建空对象 → ② 填充属性 → ③ 调用魔术方法。
**
__sleep()
**:- 在序列化之前调用,返回需要序列化的属性名数组。
- 常用于清理敏感数据(如密码)。
**
__wakeup()
**:- 在反序列化之后调用,用于恢复资源连接或初始化操作。
- 例如重新连接数据库或初始化未序列化的属性。
1 | class User { |
**
__construct()
**:(构造函数)- 执行时机:反序列化时不会触发构造函数,实例化对象时调用
- 注意:
- 对象的构造与反序列化是两个独立的过程。
- 若需要在反序列化后执行初始化逻辑,应使用
__wakeup()
或__unserialize()
。
__destruct()
(析构函数)- 执行时机:在对象被销毁时调用(如脚本结束或手动
unset()
)。- new User() 触发
- serialize() 不触发
- unserialize 触发
- 潜在风险:
- 若反序列化的对象包含恶意代码,
__destruct()
可能被用于攻击(如文件删除、远程代码执行)。
- 若反序列化的对象包含恶意代码,
- 示例:
- 执行时机:在对象被销毁时调用(如脚本结束或手动
1 | class FileHandler { |
__toString()
方法
执行时机:
当对象被当作字符串使用时自动触发。常见场景包括:直接
echo
或print
对象。字符串拼接操作(例如
$str = "User: " . $obj;
)。使用字符串函数处理对象(例如
strval($obj)
或(string)$obj
)。在双引号字符串中直接插入对象(例如
"Info: $obj"
)。
核心规则:
- 必须返回一个字符串,否则会抛出
TypeError
异常。 - 如果未定义
__toString()
,直接以对象作为字符串使用会触发致命错误(Object of class X could not be converted to string
)。
- 必须返回一个字符串,否则会抛出
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class User {
private $name;
public function __construct($name) {
$this->name = $name;
}
public function __toString() {
return "User: " . $this->name;
}
}
$user = new User("Alice");
echo $user; // 输出:User: Alice
__invoke()
方法执行时机:
当尝试以调用函数的方式调用对象时触发。例如:1
2
3$obj = new MyClass();
$obj(); // 触发 __invoke()
$result = $obj(1, "arg"); // 传递参数核心规则:
- 可以定义参数和返回值,与普通函数一致。
- 如果未定义
__invoke()
,将抛出致命错误(Object of class X is not callable
)。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Adder {
private $base;
public function __construct($base) {
$this->base = $base;
}
public function __invoke($x) {
return $this->base + $x;
}
}
$adder = new Adder(10);
echo $adder(5); // 输出:15(等价于调用 $adder->__invoke(5))__call()
方法- 执行时机:
当对象调用一个不存在的方法时调用call方法 - 核心规则:
- 参数分别为调用的不存在的函数名,和传入的参数数组
1
2
3
4
5
6
7
8class User(){
public function __call($arg1, $arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User();
$test -> functionxxx('a');- 执行时机:
__callStatic
方法执行时机:
和call方法类似,当调用不存在的静态方法是调用该函数。
$test :: functionsss('a');
核心规则:
__get
方法执行时机:
当调用一个不存在的属性时调用,参数和返回值为不存在的成员属性名称。
__set
方法执行时机:
当给一个不存在的属性赋值时调用,参数和返回值为不存在的成员属性名称和赋的值。
__isset
方法执行时机:
对不可访问属性使用isset()或empty()时__isset()会被调用,参数和返回值是调用的成员属性的名称。
__unset
方法执行时机:
对不可访问属性使用unset()或empty()时__unset()会被调用,参数和返回值是调用的成员属性的名称。
__clone
方法执行时机:
当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone()
1
2$test = new User();
$newtest = clone($test)//新对象调用__clone()方法
4. 数组与基本类型的序列化
- 序列化数组或基本类型时无需类定义。
- 示例:
1
2
3
4
5
6
7$data = [
"key" => "value",
"num" => 42,
"nested" => [true, null]
];
$serialized = serialize($data);
// 输出:a:3:{s:3:"key";s:5:"value";s:3:"num";i:42;s:6:"nested";a:2:{i:0;b:1;i:1;N;}}
5.POP链构造
- 例题
1 |
|
- pop链构造
1 |
|
6. 安全性问题
- 反序列化漏洞:反序列化用户可控的字符串可能导致代码执行(例如通过
__destruct()
或__wakeup()
注入恶意逻辑)。 - 防御措施:
- 避免反序列化不可信来源的数据。
- 使用
json_encode()
/json_decode()
替代(但无法处理对象)。
7. 常见用途
- 会话存储:PHP 默认使用序列化存储
$_SESSION
数据。 - 缓存:将对象缓存到文件或数据库中。
- 跨请求传输:通过字符串传递复杂数据。
8. 注意事项
- 类定义必须存在:反序列化对象时需要类已加载。
- 反序列化未定义类的行为
- 当使用
unserialize()
反序列化一个对象时,PHP会尝试根据序列化字符串中记录的类名重建对象。 - 如果对应的类未在当前作用域中定义(例如未通过
include
或autoload
加载),PHP不会报错,但会将对象转换为__PHP_Incomplete_Class
的实例。 - 此时,原始类的属性、方法均无法直接访问,对象处于“不完整”状态。
- 当使用
- 性能:序列化大对象可能消耗较多资源。
- 版本兼容:类结构变化可能导致反序列化失败(如属性增减)。
9. PHP 反序列化中的 字符串逃逸(Serialized String Escape)
是一种利用序列化字符串的格式特性,通过修改字符长度或结构来篡改反序列化结果的漏洞利用技术。常见于程序对序列化字符串进行 字符替换或过滤 后未正确处理长度字段的情况。
漏洞原理
PHP 序列化字符串的格式严格依赖 长度标注,例如 s:5:"value"
表示字符串长度为 5。若程序在处理序列化数据时 修改了原始字符(如过滤、替换、转义),但未同步更新长度字段,会导致反序列化解析器错误地读取后续内容,从而构造恶意对象。
利用步骤(以字符替换为例)
场景示例
假设程序对用户输入的序列化字符串执行以下操作:
1 | // 将 'x' 替换为 'xx'(字符数量翻倍) |
攻击目标
通过构造特殊输入,使反序列化后的字符串包含恶意属性或对象。
构造步骤
确定替换规则
确认程序对序列化字符串的修改逻辑(如x → xx
)。计算逃逸长度
假设需要逃逸的恶意代码为";s:3:"age";i:200;}
,长度为 18 字符。
每替换一个x
会增加 1 个字符,因此需要构造x
的数量N
,使得:
替换后的逃逸字符数 = 原始逃逸字符长度
即2N = 18 → N = 9
。构建恶意序列化字符串
构造前缀部分,利用替换规则覆盖后续字段:1
2
3
4// 原始输入(替换前)
s:9:"xxxxxxxxx";s:3:"age";i:100;}
// 替换后变为('x' → 'xx')
s:9:"xxxxxxxxxxxxxxxxxx";s:3:"age";i:100;}此时,反序列化解析器会:
- 读取
s:9:"xxxxxxxxxxxxxxxxxx"
(实际长度 18,但标注为 9,导致解析错误) - 继续解析后续内容
s:3:"age";i:200;}
作为新字段,覆盖原有age
值。
- 读取
完整示例
漏洞代码
1 | class User { |
1 | // 原始输入(替换前) |
反序列化结果
1 | User Object ( |
常见利用场景
- 属性覆盖
通过逃逸修改对象属性值(如权限字段is_admin
)。 - 对象注入
逃逸后注入新的对象,触发__destruct()
或__wakeup()
等魔术方法。 - 类型混淆
修改字段类型(如将字符串改为数组,绕过某些检查)。
防御措施
- 避免直接反序列化用户输入
使用json_encode/json_decode
替代。 - 严格校验序列化数据
检查格式合法性,限制反序列化类白名单。 - 安全替换字符
替换字符后同步更新长度字段(如preg_replace_callback
)。 - 禁用危险魔术方法
避免在__wakeup()
或__destruct()
中执行敏感操作。
通过字符串逃逸,攻击者可绕过正常逻辑实现数据篡改或代码执行。在 CTF 题目中,需结合替换规则和长度计算精心构造 payload。
10.字符串引用
- 使enter的值永远等于secret的值,即使在反序列化后值发生改变,另一个值同步改变。
1 |
|
11.Session 反序列化
- 利用session写入和读取的格式不同导致的反序列化漏洞
- 利用读取时‘|’后的对象进行反序列化进行反序列化注入
12.phar文件反序列化漏洞
- 利用读取文件函数的伪协议phar,可以解析phar文件,且解析时会对phar协议文件头中的meta-data元数据区进行反序列化获取信息。导致的反序列化漏洞。
- 需要配合文件上传上传pahr文件,由于是伪协议读取,因此不一定需要.phar后缀。
- 想要利用php生成phar文件需要将ini文件中设置phar.readonly=off。
完整示例
1 | class Product { |
通过掌握这些基础,可以更安全高效地利用 PHP 的序列化功能。