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 ,序列化后的格式如下:

0x03 public、private与protect类型

private与protect变量和public变量不同,在序列化时字段名称不一样。

public属性

public属性在序列化时不需要添加额外的说明。

private属性

private属性只能在其被定义的类内部访问,且不会被继承,在属性前加上类名,即 %00className%00 用于标定其是私有的。

protected属性

protected属性可以在父类和子类中访问,变量前添加 %00*%00 用于标定其是受保护的。

序列化长度问题

php序列化属性值时,如果是private或者protected会自动在类名两边添加一个空字节,如果是url编码用%00,如果是ASCII编码用\00,都是表示一个空字节。

  1. %00User%00sex 表示 private
  2. %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()

__destruct()

__toString()

场景示例
使用 echoprint 输出对象echo $obj;
字符串拼接“Hello " . $obj;
在双引号字符串中嵌入对象“User: $obj”;
显式类型转换(string) $obj;
传递给期望字符串的函数(PHP 8.0+)strlen($obj)(不推荐,但可行)

✅ 注意:var_dump($obj)print_r($obj)json_encode($obj) 不会调用 __toString()

__call()

__callStatic()

__invoke()

__wakeup()

__get()

__set()

__isset()

__unset()

__sleep()

0x05 绕过方法

绕过__wakeup(CVE-2016-7124)

序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过 __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":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

绕过部分正则

  1. 利用加号绕过
<?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
  1. 将对象包含在数组中
<?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 限制

0x07 工具

PHPGGC:PHP通用小工具链

该工具支持的 gadget 链包括:CodeIgniter4、Doctrine、Drupal7、Guzzle、Laravel、Magento、Monolog、Phalcon、Podio、Slim、SwiftMailer、Symfony、WordPress、Yii 和 ZendFramework

0x08 反序列化漏洞

介绍

PHP 反序列化漏洞是一种常见的安全漏洞,当应用程序使用 unserialize() 函数处理用户可控的数据时,攻击者可能通过构造恶意的序列化字符串,在反序列化过程中触发危险操作(如执行任意代码、文件读写、SQL 注入等)。

反序列化漏洞出现得满足两个前提:

  1. unserialize 的参数可控。

  2. 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。

常见漏洞:

利用链(POP Chain)

现代 PHP 应用通常不会在用户类中直接包含危险函数,但可以通过 属性控制 + 魔术方法 + 类继承/组合 构造“利用链”(Property-Oriented Programming, POP Chain)。

例如:

这样即使没有直接危险函数,也能通过链式调用触发漏洞。

Contents