February 26, 2024

pickle反序列化

pickle简介

可序列化的对象

使用 pickle 序列化时,可以处理以下对象:

关于模块

在Python中,模块(module) 是一个包含Python代码的文件,其扩展名通常为.py。每个模块都定义了一个命名空间,里面可以包含变量、函数、类等。模块可以通过import语句引入到其他Python代码中,以便使用其中定义的内容。

在模块的全局作用域中定义的函数(通过 def)和类(通过 class)是可以被 pickle 序列化和反序列化的。

pickle 反序列化一个对象时,它依赖于该对象的全限定名(fully qualified name),也就是模块名+函数名或类名。这样,pickle 在反序列化时,能通过模块名找到函数或类的定义。

object.__reduce__(self) 函数

在开发自定义类时,可以通过重写 object.__reduce__() 方法来自定义序列化行为。

工作原理

object._reduce_()返回一个元组 (callable, (args, …), state)pikle在反序列化时会根据这个元组来重建对象:

注意这里就是rce的点了,可调用对象有很多。

__reduce_ex__(self, protocol)

功能:这是 __reduce__() 的一个更通用的版本,支持传入 protocol 参数。protocol 表示序列化时使用的 pickle 协议版本。

何时使用:当你希望根据不同的 pickle 协议版本调整序列化行为时,可以使用 __reduce_ex__() 方法。pickle 模块会优先调用 __reduce_ex__(),如果未定义这个方法,才会调用 __reduce__()

注意他会优先调用

__getstate__(self)

功能:这个方法用于返回对象的内部状态,pickle 在序列化对象时会调用它。通常返回的状态是一个可序列化的对象,比如字典、列表、元组等,表示对象的内部数据。

有点像__wake_up()

何时使用:当你想要控制对象序列化时的状态,可以使用 __getstate__() 方法。你可以选择性地排除不需要序列化的属性或者动态计算需要序列化的状态。

1
2
3
4
5
6
7
8
9
10
11
class MyClass:
def __init__(self, value):
self.value = value
self.secret = "secret_value" # 不想被序列化的属性

def __getstate__(self):
# 返回对象的状态,去掉了 secret
state = self.__dict__.copy()
del state['secret']
return state

object._setstate_(self)

在反序列化后自动调用,用于恢复对象的额外状态。

功能:当对象被反序列化时,pickle 会调用 __setstate__(),并将 state 作为参数传递进来。这个 state 通常是由 __getstate__() 返回的状态。

何时使用:如果你想要自定义对象在反序列化时的重建过程,或者需要重新设置某些属性,可以使用 __setstate__() 方法。

destruct() XD

1
2
3
4
5
6
7
8
9
10
class MyClass:
def __init__(self, value):
self.value = value
self.secret = None

def __setstate__(self, state):
# 自定义反序列化时的行为,恢复对象状态
self.__dict__.update(state)
self.secret = "restored_secret"

在这个例子中,__setstate__() 方法在对象反序列化时恢复对象状态,并重新赋值 secret

__getnewargs__(self)

功能:这个方法允许你为对象的反序列化返回一个用于 __new__() 的参数元组。它控制对象在反序列化时,传递给 __new__() 方法的参数。

何时使用:如果你的类需要通过 __new__() 来创建对象,并且这个对象初始化时需要特定的参数(而不仅仅是 __init__()),你可以使用 __getnewargs__()

1
2
3
4
5
6
7
8
class MyClass:
def __init__(self, value):
self.value = value

def __getnewargs__(self):
# 返回的参数将被传递给 __new__
return (self.value,)

__getnewargs_ex__(self)

1
2
3
4
5
6
7
8
9
class MyClass:
def __init__(self, value, name):
self.value = value
self.name = name

def __getnewargs_ex__(self):
# 返回用于 __new__ 的位置参数和关键字参数
return (self.value,), {'name': self.name}

__new__(cls, *args, **kwargs)

功能__new__() 是用于创建类实例的构造器方法,它在对象创建时会被首先调用。pickle 在反序列化时有时会调用 __new__() 来创建新的对象,而不是通过 __init__()

何时使用:当类的实例需要自定义的创建行为时,__new__() 非常有用,特别是当对象需要被反序列化时,必须通过特殊的方式创建。

__new__() 是用来创建对象的静态方法,它在对象初始化之前被调用。__new__() 方法负责分配内存,并返回一个新的对象实例。在 Python 中,__new__() 通常只用于不可变对象(如 intstrtuple),因为这些对象在创建后不能被修改,必须通过 __new__() 返回新的实例。

详解:

1
2
3
4
5
6
7
8
9
10
class MyClass:
def __new__(cls, *args, **kwargs):
# 创建并返回一个新对象
instance = super().__new__(cls)
return instance

def __init__(self, value):
# 初始化对象的属性
self.value = value

详解__new__()

首先明确一点:

在 Python 中,类的实例化分为两步:创建对象初始化对象__new__()__init__() 分别负责这两部分。

1
2
3
4
5
6
7
8
9
10
class MyClass:
def __new__(cls, *args, **kwargs):
# 创建并返回一个新对象
instance = super().__new__(cls)
return instance

def __init__(self, value):
# 初始化对象的属性
self.value = value

例子一目了然

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 pickle

class Point:
def __new__(cls, x, y):
# 创建一个新的实例,并设置属性
instance = super().__new__(cls)
# 使用 __new__ 设置属性,确保不可变性
instance._x = x
instance._y = y
return instance

def __init__(self, x, y):
# 因为属性已经在 __new__ 中设置,所以这里不需要做任何事情
pass

@property
def x(self):
return self._x

@property
def y(self):
return self._y

def __getnewargs__(self):
# 返回传递给 __new__ 的参数,以便反序列化时重建对象
return (self._x, self._y)

# 实例化一个 Point 对象
point = Point(3, 4)

# 使用 pickle 序列化和反序列化 Point 对象
serialized_point = pickle.dumps(point)
deserialized_point = pickle.loads(serialized_point)

# 检查反序列化后的对象
print(f"Point: ({deserialized_point.x}, {deserialized_point.y})")

我们在 __new__() 方法中创建了 Point 对象,并设置了 _x_y 属性。这样,__init__() 就不会修改这些属性,确保对象是不可变的(虽然在 Python 中的严格不可变性需要更多的措施,比如通过 __slots__ 限制修改,但此处已展示核心思想)。

虽然定义了 __init__(),但它没有实际做任何操作,因为对象的属性已经在 __new__() 中设置好了。这样可以避免初始化时再次修改属性。

**__getnewargs__()**:

对象在正常使用过程中是不可变的,但通过特殊的机制(如 pickle 的反序列化),对象的内部状态可以被“重新构建”或“重设”。这是因为 pickle 在反序列化时调用了 __new__()__getnewargs__() 等特殊方法,允许对象被重新创建。

改一下

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import pickle

class Point:
def __new__(cls, x, y):
# 创建一个新的实例,并设置属性
instance = super().__new__(cls)
instance._x = x
instance._y = y
return instance

def __init__(self, x, y):
pass # 属性已经在 __new__ 中设置,所以这里不做任何操作

@property
def x(self):
return self._x

@property
def y(self):
return self._y

def __getnewargs__(self):
# 返回传递给 __new__ 的参数,pickle 在反序列化时会调用它
return (self._x, self._y)

# 实例化一个 Point 对象
point = Point(3, 4)
print(f"Original Point: ({point.x}, {point.y})")

# 使用 pickle 序列化 Point 对象
serialized_point = pickle.dumps(point)

# 使用 Point 反序列化
deserialized_point = pickle.loads(serialized_point)
print(f"Deserialized Point: ({deserialized_point.x}, {deserialized_point.y})")


# 定义修改后的类,通过 __getnewargs__ 修改反序列化时的属性
class ModifiedPoint(Point):
def __getnewargs__(self):
# 在反序列化时返回不同的参数以修改属性
return (10, 20)

# 序列化 ModifiedPoint 对象并反序列化,确保在序列化和反序列化时使用 ModifiedPoint 类
modified_point = ModifiedPoint(10, 20)
serialized_modified_point = pickle.dumps(modified_point)

# 反序列化 ModifiedPoint 对象
deserialized_modified_point = pickle.loads(serialized_modified_point)
print(f"Modified Point: ({deserialized_modified_point.x}, {deserialized_modified_point.y})")

关键:

不可变对象设计:通过 __new__() 和属性封装的方式,创建了一个在常规使用中不可修改属性的对象。

反序列化的特殊性pickle 通过 __getnewargs__() 等特殊方法,可以在反序列化时给对象不同的初始参数,允许重新设置对象的属性。尽管对象被设计为不可变,但通过 pickle 序列化和反序列化,我们仍然可以创建带有不同状态的对象。

覆盖哦

这里已经核心操作了

ModifiedPoint(Point)

pickleR 操作码

pickle过程详细解读

20200320230631-6204866e-6abc-1

20200320230711-7972c0ea-6abc-1

opcode最好用0版本的

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
# 'abcd'
b'\x80\x03X\x04\x00\x00\x00abcdq\x00.'

# \x80:协议头声明 \x03:协议版本
# \x04\x00\x00\x00:数据长度:4
# abcd:数据
# q:储存栈顶的字符串长度:一个字节(即\x00)
# \x00:栈顶位置
# .:数据截止

pickletools

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

0: \x80 PROTO 3
2: c GLOBAL 'builtins exec'
17: q BINPUT 0
19: X BINUNICODE "key1=b'1'\nkey2=b'2'"
43: q BINPUT 1
45: \x85 TUPLE1
46: q BINPUT 2
48: R REDUCE
49: q BINPUT 3
51: . STOP
highest protocol among opcodes = 2

漏洞

利用

初步认识: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)

如何手写opcode

常用opcode

opcode 描述 具体写法 栈上的变化 memo上的变化
c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) c[module]\n[instance]\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个None N 获得的对象入栈
S 实例化一个字符串对象 S’xxx’\n(也可以使用双引号、'等python字符串形式) 获得的对象入栈
V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈
I 实例化一个int对象 Ixxx\n 获得的对象入栈
F 实例化一个float对象 Fx.x\n 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 .
( 向栈中压入一个MARK标记 ( MARK标记入栈
t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pn\n 对象被储存
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新

此外, TRUE 可以用 I 表示: b'I01\n'FALSE 也可以用 I 表示: b'I00\n' ,其他opcode可以在pickle库的源代码中找到。
由这些opcode我们可以得到一些需要注意的地方:

拼接opcode

将第一个pickle流结尾表示结束的 . 去掉,将第二个pickle流与第一个拼接起来即可。

About this Post

This post is written by void2eye, licensed under CC BY-NC 4.0.

#Web