Python 反序列化漏洞
什么是Python的序列化与反序列化
什么是Python的序列化
众所周知,Python是一种面向对象的语言。在Python中,一切皆对象。数字,字符串,函数,模块等等,都是对象。对象的本质是内存中的一个数据结构,包含属性(数据)和方法(操作)。
由于Python中对象的形式多种多样,变化万千,在存储时按照原本的形式来存储过于庞大且麻烦,所以我们就需要把Python对象转换为可存储或可传输的标准化格式。将Python对象转换为可存储或传输的标准化格式(如字节流、字符串)的过程,就叫做Python的序列化。
什么是Python的反序列化
既然为了存储和传输方便,将Python对象通过序列化的方式转换成了一种标准化的格式,那么在程序执行时,我们就需要把标准化格式还原为Python对象,而这个过程,就叫做Python的反序列化。
序列化的本质
- 序列化的本质是将对象的状态或数据结构转换为一种通用格式(如二进制,JSON,XML),使其可以保存到文件、数据库或通过网络传输。
- 反序列化是逆过程,将序列化后的数据还原为原始对象。
Python中序列化的实现过程
Python庞大的库,为Python强大的功能实现提供支撑和保障。同样的,Python中序列化和反序列化的实现也是通过一些库和模块来实现的。
- pickle模块 作为Python的原生模块,pickle支持几乎所有的Python对象 使用pickle进行序列化生成的是二进制格式数据
代码示例:
|
|
- json模块 json可以生成人类可读的JSON字符串 但json只支持基础类型(字典、列表、字符串、数字等),不支持Python特有对象(如类实例)的序列化。
代码示例:
|
|
- PyYAML库 YAML(YAML Ain’t Markup Language)是一种人类可读的数据序列化格式,常用于配置文件和数据交换。Python 中通过 PyYAML 库支持 YAML。
序列化实现:
|
|
输出:
|
|
保存/读取文件:
|
|
- 使用 Protocol Buffers(protobuf)实现序列化 Protocol Buffers 是 Google 开发的高效二进制序列化格式,适合高性能通信和跨语言数据交换。需预先定义数据结构(.proto 文件)。
Python中反序列化实现过程
在以上Python序列化实现过程中,使用pickle模块和Pyyaml库,在实现反序列化的过程中存在安全风险。我们先来看看他们的反序列化过程是如何实现的
YAML 反序列化实现
YAML的反序列化过程通过PyYAML库实现,本质是将YAML文本解析为Python对象。默认的 yaml.load() 方法支持动态构造Python对象(包括类实例),但这也带来了安全风险。
反序列化步骤:
1.安装依赖
|
|
2.基础反序列化(安全模式):
|
|
3.动态对象反序列化(危险!):
YAML 可以通过标签(!!python/object)动态构造 Python 对象,例如:
|
|
危险的反序列化代码:
|
|
安全实践
始终使用 yaml.safe_load():仅解析基础类型(字典、列表、字符串、数字),禁止动态对象构造。
禁用危险标签:若必须解析对象,需自定义加载器,过滤允许的类。
|
|
Pickle 反序列化实现
Pickle 的反序列化通过 pickle.load() 或 pickle.loads() 实现,其底层会重建对象的完整状态,包括调用__reduce__方法(若存在),从而可能执行任意代码。
反序列化步骤:
1.序列化一个对象:
|
|
2.基础反序列化:
|
|
3.恶意反序列化示例:
|
|
安全风险根源:
__reduce__方法:在反序列化时自动调用,返回一个元组(函数、参数),执行函数。
任意代码执行:攻击者可构造恶意__reduce__方法,执行危险操作(如删除文件、反弹Shell)。
防御方法
避免反序列化不可信数据:永远不要对来源未知的数据使用 pickle.load()。
限制允许的类(白名单):
|
|
反序列化漏洞的成因
我们上面提到了YAML和pickle的反序列化过程以及他们的风险漏洞,这里来总结一下。
YAML的反序列化漏洞
造成YAML反序列化漏洞的原因是由于过于相信YAML数据来源,在反序列化的过程中使用了一些危险标签,使得攻击者利用这些标签使用 yaml.load() 方法来动态构造Python对象,导致程序错误地调用了 os.system() 进行命令执行
Pickle 的反序列化漏洞
造成pickle反序列化漏洞的原因是在反序列化过程中,存在调用__reduce__方法的可能,这一方法使得攻击者可以通过构造恶意类,在__reduce__中返回一个危险的可调用对象(如 os.system),从而在反序列化时触发任意代码执行。
为什么__reduce__危险?
- 完全控制反序列化逻辑:
__reduce__允许攻击者指定任意函数和参数,且反序列化时自动执行。
- 绕过对象状态限制:
即使目标代码中没有恶意类,攻击者也可以构造包含危险函数的__reduce__。
Python反序列化漏洞的利用
我们已经讨论过Python中的反序列化漏洞是由于错误使用yaml.load()、危险标签和__reduce__,导致的命令执行漏洞,因此,我们的利用点也着眼于此。
yaml漏洞的利用
对于PyYaml<5.1版本下的漏洞,其主要原因主要出现在下面五个python标签:
|
|
漏洞成因:
由于上面提到的五个标签,在constructor.py文件被加载器解析导致,攻击者利用这类标签可以达到任意命令执行,以及验证绕过等漏洞的利用。
payload:
|
|
在5.1之后的yaml中load函数被限制使用了,会被警告提醒加上一个参数 Loader
针对不同的需要,选择不同的加载器,有以下几种加载器
|
|
如果说指定的加载器是UnsafeConstructor 或者Constructor,那么利用方式就照旧
payload
我们可以用python的内置函数eval(或者exec)来执行代码,用map来触发函数执行,用tuple将map对象转化为元组输出来(当然用list、frozenset、bytes都可以),用python写出来如下
|
|
变为yaml
|
|
除此之外网上还有很多大佬有其他的payload
|
|
|
|
|
|
参考:
Pickle 漏洞利用
漏洞常见出现地方
- 通常在解析认证
token,session的时候. 现在很多 Web 服务都使用redis、mongodb、memcached等来存储session等状态信息. - 可能将对象 Pickle 后存储成磁盘文件.
- 可能将对象 Pickle 后在网络中传输.
基本 Payload
|
|
Marshal 反序列化
由于pickle无法序列化code对象, 因此在python2.6后增加了一个marshal模块来处理code对象的序列化问题.
|
|
但是marshal不能直接使用__reduce__, 因为reduce是利用调用某个callable并传递参数来执行的, 而marshal函数本身就是一个callable, 需要执行它, 而不是将他作为某个函数的参数.
Pyload(PyYaml >= 5.1)
|
|
|
|
参考:
其他:
关于callable:
在 Python 中,“callable”(可调用对象) 是指任何可以通过 () 运算符调用的对象。简单来说,如果一个对象可以像函数一样被调用(例如 obj()),它就是可调用的。以下是关于 callable 的详细解释:
1. 常见的可调用对象类型
(1) 函数(Function)
-
包括内置函数、自定义函数、Lambda 函数。
1 2 3 4 5 6 7 8 9 10python def greet(name): print(f"Hello, {name}!") # 调用函数 greet("Alice") # 输出 Hello, Alice! # Lambda 也是可调用的 add = lambda a, b: a + b print(add(3, 5)) # 输出 8
(2) 类(Class)
-
类本身是可调用的
,调用类会创建它的实例。
1 2 3 4 5 6 7python class Dog: def __init__(self, name): self.name = name # 调用类创建实例 my_dog = Dog("Buddy") # Dog 类是可调用的
(3) 方法(Method)
-
类中定义的方法(实例方法、类方法、静态方法)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17python class Calculator: def add(self, a, b): # 实例方法 return a + b @classmethod def multiply(cls, a, b): # 类方法 return a * b @staticmethod def subtract(a, b): # 静态方法 return a - b calc = Calculator() calc.add(2, 3) # 实例方法调用 Calculator.multiply(2, 3) # 类方法调用 Calculator.subtract(5, 2) # 静态方法调用
(4) 实现了 __call__ 方法的对象
-
如果一个类定义了
1__call__方法,它的实例会成为可调用对象。
1 2 3 4 5 6 7 8 9 10python class Adder: def __init__(self, x): self.x = x def __call__(self, y): return self.x + y add_5 = Adder(5) print(add_5(3)) # 输出 8(等价于 add_5.__call__(3))
(5) 其他内置可调用对象
-
生成器函数、部分函数(
1functools.partial)等。
1 2 3 4 5 6 7 8python import functools def power(base, exp): return base ** exp square = functools.partial(power, exp=2) # 部分函数 print(square(3)) # 输出 9
2. 如何判断一个对象是否可调用?
使用内置函数 callable() 可以检测对象是否可调用:
|
|
3. 不可调用的对象示例
-
基础数据类型:整数、字符串、列表等。
1 2 3python x = 42 x() # 报错:'int' object is not callable -
未实现
__call__的实例:1 2 3 4 5 6python class Cat: pass my_cat = Cat() my_cat() # 报错:'Cat' object is not callable
4. Callable 的实际应用场景
-
装饰器(Decorator):
-
装饰器本身必须是可调用对象(函数或类)。
1 2 3 4 5 6 7 8 9 10 11 12python def logger(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper @logger def say_hello(): print("Hello!") say_hello() # 输出 "Calling say_hello" 和 "Hello!"
-
-
动态调用函数:
-
通过变量名动态调用函数。
1 2 3 4 5 6python def do_operation(op, a, b): operations = {"add": lambda x, y: x + y, "mul": lambda x, y: x * y} return operations[op](a, b) print(do_operation("add", 3, 5)) # 输出 8
-
-
回调机制:
-
将函数作为参数传递,在特定事件发生时调用。
1 2 3 4 5 6 7 8 9python def on_button_click(callback): print("按钮被点击了!") callback() def show_message(): print("执行回调函数") on_button_click(show_message)
-
总结
- 可调用对象是 Python 的核心概念之一,包括函数、类、方法和实现了
__call__的实例。 - 使用
callable()可以快速检测对象是否可调用。 - 理解 callable 是掌握装饰器、动态编程和回调机制的关键。