PHP反序列化漏洞总结
0x01 简介
PHP 的序列化(Serialization)和反序列化(Unserialization)是将 PHP 数据结构(如数组、对象等)转换为字符串格式,以及将该字符串还原回原始数据结构的过程。
1. 序列化(serialize)
作用:将 PHP 变量(如数组、对象、标量等)转换成一个可存储或传输的字符串表示形式。
函数:serialize($value)
示例:
$data = ['name' => 'Alice', 'age' => 30];
$serialized = serialize($data);
echo $serialized;
// 输出:a:2:{s:4:"name";s:5:"ALICE";s:3:"age";i:30;}
这个字符串包含了类型、长度和值的信息,可以保存到文件、数据库或通过网络传输。
2. 反序列化(unserialize)
作用:将序列化后的字符串还原为原来的 PHP 变量。
函数:unserialize($str)
示例:
$serialized = 'a:2:{s:4:"name";s:5:"Alice";s:3:"age";i:30;}';
$data = unserialize($serialized);
print_r($data);
// 输出:
// Array
// (
// [name] => Alice
// [age] => 30
// )
0x02 序列化格式
其中php_serialize的实现在 php-src/ext/standard/var.c 中,主要函数为 php_var_serialize_intern ,序列化后的格式如下:
- boolean
b:<value>;b:1;// trueb:0;// false
- integer
i:<value>;i:66
- double
d:<value>;d:66.88
- null
N;
- string
s:<length>:"<value>";s:1:"s";
- array
a:<length>:{key, value};a:1:{s:4:"key1";s:6:"value1";}<==>array("key1" => "value1");
- object
O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>};
- reference
- 指针类型
R:reference;O:1:"A":2:{s:1:"a";i:1;s:1:"b";R:2;}$a = new A();$a->a=1;$a->b=&$a->a;
0x03 public、private与protect类型
private与protect变量和public变量不同,在序列化时字段名称不一样。
public属性
public属性在序列化时不需要添加额外的说明。
private属性
private属性只能在其被定义的类内部访问,且不会被继承,在属性前加上类名,即 %00className%00 用于标定其是私有的。
protected属性
protected属性可以在父类和子类中访问,变量前添加 %00*%00 用于标定其是受保护的。
序列化长度问题
php序列化属性值时,如果是private或者protected会自动在类名两边添加一个空字节,如果是url编码用%00,如果是ASCII编码用\00,都是表示一个空字节。
%00User%00sex表示 private%00*%00money表示 protected
<?php
class Test {
public $name;
private $sex;
protected $money;
public function __construct($name, $sex, $money) {
$this->name = $name;
$this->sex = $sex;
$this->money = $money;
}
}
$test = new Test("test", true, 188);
echo serialize($test);
// O:4:"Test":3:{s:4:"name";s:4:"test";s:9:"%00Test%00sex";b:1;s:8:"%00*%00money";i:188;}
0x04 魔术方法
简介
| 魔术方法 | 介绍 | 自动调用 |
|---|---|---|
__construct() | 当一个对象创建时被调用,反序列化不触发 | 序列化时自动调用 |
__destruct() | 当一个对象销毁时被调用,反序列化自动调用 | 反序列化时自动调用 |
__toString() | 当一个对象被当作一个字符串使用时触发 | ✕ |
__call() | 在对象上下文中调用不可访问的方法时触发 | ✕ |
__callStatic() | 在静态上下文中调用不可访问的方法时触发 | ✕ |
__invoke() | 当一个对象被当作函数调用时触发 | ✕ |
__wakeup() | 执行反序列化时,先会调用这个函数 | 反序列化时自动调用 |
__get() | 从类中私有或不存的属性读取数据时触发 | ✕ |
__set() | 给类中私有或不存的属性写入数据时触发 | ✕ |
__isset() | 在私有或不存的属性上调用 isset() 或 empty() 触发 | ✕ |
__unset() | 在私有或不存的属性上调用 unset() 时触发 | ✕ |
__sleep() | 序列化时自动触发 | 序列化时自动调用 |
__construct()
__construct()方法是类的构造方法,当一个对象创建时被自动调用,通常用于创建新对象时的初始化(比如分配资源、连接数据库等)当对一个对象进行反序列化(
unserialize())时,PHP 不会自动调用__construct()方法。反序列化是恢复已有状态,不是“新建”,所以不应重复执行构造逻辑。
示例:
<?php class Test { public $name; public function __construct($name) { $this->name = $name; echo "call __construct()"; } } serialize(new Test("test")); // call __construct()
__destruct()
__destruct()方法在对象被销毁时自动调用,常用于执行清理工作,如关闭文件句柄、释放资源、记录日志等。在反序列化(
unserialize())时,PHP 会自动调用__destruct()方法。示例:
<?php class Test { public $name; public function __destruct() { echo "call __destruct()"; } } unserialize('O:4:"Test":1:{s:4:"name";s:4:"test";}'); // call __destruct()
__toString()
__toString()方法用于定义当对象被当作字符串使用时应如何转换为字符串。在反序列化(
unserialize())时,PHP 不会自动调用__toString()方法。触发场景
| 场景 | 示例 |
|---|---|
使用 echo 或 print 输出对象 | echo $obj; |
| 字符串拼接 | “Hello " . $obj; |
| 在双引号字符串中嵌入对象 | “User: $obj”; |
| 显式类型转换 | (string) $obj; |
| 传递给期望字符串的函数(PHP 8.0+) | strlen($obj)(不推荐,但可行) |
✅ 注意:var_dump($obj)、print_r($obj)、json_encode($obj) 不会调用 __toString()。
示例:
<?php class Test { public $name; public function __toString() { echo "call __toString()"; return ""; } } $obj = unserialize('O:4:"Test":1:{s:4:"name";s:4:"test";}'); echo $obj; // call __toString() $b = $obj . 'a'; // call __toString() $c = "Test: $obj"; // call __toString() $d = (string) $obj; // call __toString()
__call()
__call()方法用于拦截对不存在(或不可访问)的非静态方法的调用。它能让你动态处理方法调用,实现灵活的 API 设计、代理模式、链式调用等高级功能。示例:
<?php class Test { public $name; public function __call($method, $args) { echo "call {$method}({$args[0]})"; } } $obj = unserialize('O:4:"Test":1:{s:4:"name";s:4:"test";}'); $obj->test(123); // call test(123)
__callStatic()
__callStatic()方法用于拦截对不存在(或不可访问)的静态方法的调用。它是__call()的静态版本,专为类(而非对象实例)设计。unserialize()不会调用__callStatic(),反序列化是对象重建过程,不涉及“调用静态方法”。示例:
<?php class Test { public $name; public static function __callStatic($method, $args) { echo "call {$method}({$args[0]})"; } } Test::test(123); // call test(123)
__invoke()
__invoke()方法在当一个对象被当作函数调用时触发。在反序列化(unserialize)的上下文中,
__invoke()本身不会被自动触发,但在特定条件下(如与其他魔术方法结合),它可以成为反序列化漏洞利用链(gadget chain)中的关键一环。通过其它魔术方法调用
$this();也可以触发__invoke()示例:
<?php class Test { public $name; public function __invoke() { echo "call __invoke()"; } public function __destruct() { $this(); } } $obj = unserialize('O:4:"Test":1:{s:4:"name";N;}'); // call __invoke() $obj(); // call __invoke()
__wakeup()
__wakeup()方法在对象被 反序列化(unserialize)时首先被自动调用。它主要用于恢复对象状态,但由于其自动执行的特性,也成为反序列化漏洞(Unserialize RCE)中常见的入口点之一。示例:
<?php class Test { public $name; public function __wakeup() { echo "call __wakeup()"; } } $obj = unserialize('O:4:"Test":1:{s:4:"name";N;}'); // call __wakeup()
__get()
__get()方法在当访问一个未定义的属性或访问一个 不可访问的属性(private或protected类型) 时自动调用。它常用于实现延迟加载(Lazy Loading)、动态属性、代理模式等,但也因其自动调用特性,成为反序列化漏洞(Unserialize RCE)的关键利用点之一。
__get()必须是 public 方法,返回值即为该“虚拟属性”的值。示例:
<?php class Test { public $name; private $age; public function __construct($name, $age) { $this->name = $name; $this->age = $age; } public function getName() { return $this->name; } public function __get($name) { echo "call __get()"; return $this->$name; } } $obj = unserialize(serialize(new Test("Dog", 8))); $obj->getName(); // 不会触发__get() $a = $obj->a; // call __get() $age = $obj->age; // call __get()
__set()
__set()方法用于在给未定义的属性或 不可访问的属性(private或protected类型) 赋值时自动触发。常用于实现动态属性、数据验证、代理模式等,但由于其自动调用特性,在反序列化场景中也可以成为利用连的跳板。
示例:
<?php class Test { public $name; private $age; public function __construct($name, $age) { $this->name = $name; } public function __set($name, $value) { echo "call __set({$name}, {$value})"; } } $obj = unserialize(serialize(new Test("Dog", 8))); $obj->name = "aaa"; // 不会调用__set() $obj->test = "666"; // call __set(test, 666) $obj->age = "123"; // call __set(age, 123)
__isset()
__isset()方法用于在对对象的 未定义的属性或不可访问属性(private或protected类型) 使用isset()或empty()时自动触发。它通常与__get()、__set()配合使用,以实现对动态属性的完整控制。在复杂的反序列化利用链中,它可以作为条件判断或跳板被间接调用。
__isset()必须是public方法当执行
isset($obj->prop)或empty($obj->prop)时,若$prop不存在或不可访问,则调用此方法。必须返回
bool类型(PHP 8.0+ 严格要求)在对象内部调用
isset($obj->age)时,若$age是private或protected类型,不会触发__isset()示例:
<?php class Test { public $name; private $age; public function __construct($name, $age) { $this->name = $name; } public function test() { isset($this->prop); // call __isset() isset($this->age); // 不会调用__isset() empty($this->prop); // call __isset() empty($this->age); // call __isset() } public function __isset($name) { echo "call __isset()"; } } $obj = unserialize(serialize(new Test("Dog", 8))); isset($obj->name); // 不会调用__isset() isset($obj->prop); // call __isset() isset($obj->age); // call __isset() empty($obj->prop); // call __isset() empty($obj->age); // call __isset()
__unset()
__unset()方法用于在对对象的 未定义的属性或不可访问属性(private或protected类型) 使用unset()时自动触发。它通常与__get()、__set()、__isset()配合,实现对动态属性的完整生命周期管理。必须是
public方法。当执行
unset($obj->prop)且$prop不存在或不可访问时自动调用。常用于清理动态存储的数据(如内部数组、缓存等)。
示例:
<?php class Test { public $name; private $age; public function __construct($name, $age) { $this->name = $name; $this->age = $age; } public function __unset($name) { echo "call __unset()"; } } $obj = unserialize(serialize(new Test("Dog", 8))); unset($obj->name); // 不会调用__unset() unset($obj->nonExistentProp); // call __unset() unset($obj->age); // call __unset()
__sleep()
在使用 serialize() 函数时,程序会检查类中是否存在一个 __sleep() 魔术方法,如果存在,则该方法会先被调用,然后再执行序列化操作。
示例:
<?php class Test { public $name; public function __construct($name) { $this->name = $name; } public function __sleep() { echo 'call __sleep()'; } } serialize(new Test("test")); // call __sleep()
0x05 绕过方法
绕过__wakeup(CVE-2016-7124)
序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过 __wakeup() 的执行。
版本:
- PHP5 < 5.6.25
- PHP7 < 7.0.10
示例:
<?php
class Test {
public $name;
public function __construct(){
$this->name = 'ok';
}
public function __wakeup(){
echo "call __wakeup()";
}
public function __destruct(){
echo $this->name;
}
}
unserialize('O:4:"Test":1:{s:4:"name";s:2:"ok";}'); // call __wakeup()ok
以上执行 unserialize 会自动调用 __wakeup(),如果把属性个数从1改成2,则 __wakeup() 不会执行:
<?php
class Test {
public $name;
public function __construct(){
$this->name = 'ok';
}
public function __wakeup(){
echo "call __wakeup()";
}
public function __destruct(){
echo $this->name;
}
}
unserialize('O:4:"Test":2:{s:4:"name";s:2:"ok";}'); // ok
引用绕过
利用引用使两值恒等,相当于c语言的指针,通过取地址符将a的地址给b,让b指向和a相同的内存空间,共享同一块内存,所有存储的内容是相同的,一方改变另一方内容也改变。
<?php
class Test {
public $a;
public $b;
public function __construct(){
$this->a = 'abc';
$this->b= &$this->a;
}
public function __destruct(){
if($this->a===$this->b){
echo 666;
}
}
}
$a = serialize(new Test()); // 666
16进制绕过字符的过滤
O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析,可以写成:
O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
PHP7.1+反序列化对类属性不敏感
前面说了如果变量前是protected,序列化结果会在变量名前加上 \x00*\x00,但在特定版本 7.1以上(>=7.2) 则对于类属性不敏感,比如下面的例子 name 前面即使没有 \x00*\x00 也依然会输出 ok。
<?php
class Test {
protected $name;
public function __construct(){
$this->name = 'ok';
}
public function __destruct(){
echo $this->name;
}
}
unserialize('O:4:"Test":1:{s:4:"name";s:2:"ok";}'); // ok
绕过部分正则
- 利用加号绕过
<?php
class Test {
public $name;
public function __construct(){
$this->name = 'ok';
}
public function __destruct(){
echo $this->name;
}
}
// O:4 替换为 O:+4
unserialize('O:+4:"Test":1:{s:4:"name";s:2:"ok";}'); // ok
- 将对象包含在数组中
<?php
class Test {
public $name;
public function __construct(){
$this->name = 'ok';
}
public function __destruct(){
echo $this->name;
}
}
unserialize('a:1:{i:0;O:4:"Test":1:{s:4:"name";s:2:"ok";}}'); // ok
0x06 限制
- PHP 7.0+ 可使用
unserialize($data, ['allowed_classes' => ['User']])限制可反序列化的类。
0x07 工具
PHPGGC:PHP通用小工具链
该工具支持的 gadget 链包括:CodeIgniter4、Doctrine、Drupal7、Guzzle、Laravel、Magento、Monolog、Phalcon、Podio、Slim、SwiftMailer、Symfony、WordPress、Yii 和 ZendFramework
0x08 反序列化漏洞
介绍
PHP 反序列化漏洞是一种常见的安全漏洞,当应用程序使用 unserialize() 函数处理用户可控的数据时,攻击者可能通过构造恶意的序列化字符串,在反序列化过程中触发危险操作(如执行任意代码、文件读写、SQL 注入等)。
反序列化漏洞出现得满足两个前提:
unserialize的参数可控。代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。
常见漏洞:
- 反序列化RCE
- Cookie反序列化身份伪造
- 反序列化任意文件读取
- 反序列化任意文件写入
- 反序列化信息泄露
利用链(POP Chain)
现代 PHP 应用通常不会在用户类中直接包含危险函数,但可以通过 属性控制 + 魔术方法 + 类继承/组合 构造“利用链”(Property-Oriented Programming, POP Chain)。
例如:
- 找到一个类 A 的
__destruct()调用了$this->obj->doSomething() - 而类 B 的
doSomething()会执行eval($this->data) - 攻击者让 A 的
$obj指向 B 实例,并控制$data
这样即使没有直接危险函数,也能通过链式调用触发漏洞。