Python Pickle 反序列化漏洞学习与防护

一、漏洞背景

在 Python 中,pickle 是一个常见的序列化模块,可以把 Python 对象转换成字节流,也可以把字节流还原成对象。

序列化常用于:

  • 本地缓存对象
  • 保存程序运行状态
  • 进程间传递对象
  • 简单的数据持久化

但如果程序对不可信数据执行 pickle.loads()pickle.load(),就可能触发反序列化漏洞。攻击者可以构造特殊对象,让程序在反序列化过程中执行预设函数,从而造成命令执行风险。

本文只在本地靶场和授权环境中演示,用于理解漏洞原理和防护思路。

二、Pickle 基础用法

1. 序列化

1
2
3
4
5
6
7
8
9
10
import pickle

data = {
"name": "Alice",
"age": 25,
"city": "New York"
}

data_bytes = pickle.dumps(data)
print(data_bytes)

pickle.dumps() 会将对象转换成字节流。

2. 反序列化

1
2
3
4
import pickle

data = pickle.loads(data_bytes)
print(data)

pickle.loads() 会把字节流恢复成原来的 Python 对象。

如果数据来源可信,这个过程通常没有问题。但如果 data_bytes 来自用户输入、Cookie、请求参数、上传文件或外部接口,就需要非常谨慎。

三、漏洞原理

Pickle 反序列化风险的核心在于:对象可以通过特殊方法控制反序列化时的还原逻辑。

其中最常见的是 __reduce__() 方法。

当对象被 pickle 反序列化时,__reduce__() 可以返回一个可调用对象和参数。反序列化时 Python 会调用该对象,并传入参数。

简单理解:

1
攻击者构造对象 -> 序列化成字节流 -> 服务端 loads -> 自动调用指定函数

这也是为什么不应该对不可信数据使用 Pickle。

四、本地演示

下面用一个无害命令演示触发过程。

1
2
3
4
5
6
7
8
9
10
11
import pickle
import os


class Demo:
def __reduce__(self):
return (os.system, ("echo pickle_demo",))


payload = pickle.dumps(Demo())
pickle.loads(payload)

运行结果会执行:

1
echo pickle_demo

这个例子说明:反序列化过程并不只是“恢复数据”,它可能执行函数调用。

五、Base64 传输场景

在 Web 题目或接口中,Pickle 数据经常会被 Base64 编码后传输。

生成 payload:

1
2
3
4
5
6
7
8
9
10
11
12
import base64
import pickle
import os


class Demo:
def __reduce__(self):
return (os.system, ("echo pickle_demo",))


payload = pickle.dumps(Demo())
print(base64.b64encode(payload).decode())

服务端如果这样处理用户输入,就存在风险:

1
2
3
4
5
import base64
import pickle

raw = request.args.get("data")
obj = pickle.loads(base64.b64decode(raw))

问题不在 Base64,而在于服务端对用户可控内容执行了 pickle.loads()

六、常见危险代码特征

审计 Python Web 项目时,可以重点搜索以下代码:

1
2
3
4
5
6
7
pickle.load
pickle.loads
cPickle.load
cPickle.loads
base64.b64decode
session
cookie

高风险组合:

1
2
3
4
pickle.loads(request.data)
pickle.loads(request.form["data"])
pickle.loads(base64.b64decode(request.cookies["user"]))
pickle.load(open(user_upload_file, "rb"))

如果反序列化对象来自用户输入或上传文件,就需要重点关注。

七、CTF 中的常见思路

CTF 中常见的 Pickle 题会把序列化数据放在:

  • Cookie
  • Flask session
  • POST 参数
  • Base64 字符串
  • 上传文件

解题思路一般是:

1
找到反序列化入口 -> 判断数据编码方式 -> 构造 __reduce__ -> 编码 payload -> 触发 loads

如果题目过滤了关键字,可以尝试:

  • 使用 __import__ 动态导入模块
  • 换用不同可调用对象
  • 分析过滤逻辑是否只过滤字符串明文
  • 结合源码寻找可利用函数

但在真实业务中,不应该依赖过滤关键字来防护 Pickle 反序列化。

八、防护建议

1. 不反序列化不可信数据

最重要的原则:

1
不要对用户可控数据使用 pickle.loads()

如果只是传递普通数据,优先使用 JSON:

1
2
3
import json

data = json.loads(user_input)

JSON 只表示数据结构,不会像 Pickle 一样恢复任意 Python 对象。

2. 做数据签名

如果业务必须存储序列化数据,应至少做完整性校验,例如 HMAC 签名:

1
2
3
4
import hmac
import hashlib

signature = hmac.new(secret_key, data, hashlib.sha256).hexdigest()

服务端校验签名通过后再处理数据,避免用户篡改内容。

不过签名只能防篡改,不能改变 Pickle 本身的危险特性。

3. 限制反序列化类型

可以自定义 Unpickler 限制允许加载的类,但实现复杂,容易漏掉风险。真实业务中更推荐从设计上避免使用 Pickle 处理外部输入。

4. 最小权限运行服务

即使发生漏洞,也应降低影响:

  • Web 服务使用低权限用户运行。
  • 容器或沙箱隔离。
  • 限制文件读写权限。
  • 限制外连能力。
  • 记录异常请求日志。

九、排查思路

如果怀疑系统存在 Pickle 反序列化风险,可以按以下流程排查:

1
搜索 pickle.loads -> 确认数据来源 -> 判断是否用户可控 -> 检查是否签名校验 -> 复现验证 -> 替换为安全格式

日志上可以关注:

  • 参数中出现长 Base64 字符串。
  • Cookie 体积异常变大。
  • 服务端出现系统命令执行痕迹。
  • Web 进程产生异常子进程。
  • 反序列化报错堆栈。

十、总结

Pickle 反序列化漏洞的本质是:程序把不可信输入当成可信对象恢复,并在恢复过程中执行了对象指定的逻辑。

学习这类漏洞时,重点不是背 payload,而是理解:

  • pickle.loads() 为什么危险。
  • __reduce__() 如何影响反序列化流程。
  • Web 场景中用户输入如何进入反序列化入口。
  • 真实业务中应如何替换和加固。

在生产环境中,最稳妥的做法是:外部输入使用 JSON 等安全数据格式,避免使用 Pickle 处理用户可控内容。