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
序列化时,可以处理以下对象:
基本类型:
None
、True
、False
、整数、浮点数、复数字符串和字节数据:
str
、bytes
、bytearray
集合类型: 只包含可封存对象的
tuple
、list
、set
和dict
函数和类
定义在模块最外层的函数和类
- 例如,通过
def
定义的函数和通过class
定义的类,可以通过pickle
序列化和反序列化。 - 但匿名函数(
lambda
)不支持,因为它们不能被引用为独立的模块属性。
- 例如,通过
自定义类的实例
- 只要类对象定义在模块的全局作用域上,并且其属性(例如
__dict__
)也可序列化,pickle
就能处理这些实例。 - 另外,类还可以通过实现
__getstate__
和__setstate__
方法,来自定义序列化和反序列化行为。
- 只要类对象定义在模块的全局作用域上,并且其属性(例如
关于模块
在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在反序列化时会根据这个元组来重建对象:
callable
是一个可调用对象(通常是构造函数),用于创建新实例。(args, ...)
是传递给callable
的参数。注意,必须是元组state
是额外的信息(可选),用于恢复对象的内部状态。可以是列表或者字典。例如,当对象被反序列化时,会先调用
callable(*args)
生成对象实例,然后根据state
进一步调整对象状态。
注意这里就是rce的点了,可调用对象有很多。
__reduce_ex__(self, protocol)
功能:这是 __reduce__()
的一个更通用的版本,支持传入 protocol
参数。protocol
表示序列化时使用的 pickle 协议版本。
何时使用:当你希望根据不同的 pickle
协议版本调整序列化行为时,可以使用 __reduce_ex__()
方法。pickle
模块会优先调用 __reduce_ex__()
,如果未定义这个方法,才会调用 __reduce__()
。
注意他会优先调用
__getstate__(self)
功能:这个方法用于返回对象的内部状态,pickle
在序列化对象时会调用它。通常返回的状态是一个可序列化的对象,比如字典、列表、元组等,表示对象的内部数据。
有点像__wake_up()
何时使用:当你想要控制对象序列化时的状态,可以使用 __getstate__()
方法。你可以选择性地排除不需要序列化的属性或者动态计算需要序列化的状态。
1 | class MyClass: |
object._setstate_(self)
在反序列化后自动调用,用于恢复对象的额外状态。
功能:当对象被反序列化时,pickle
会调用 __setstate__()
,并将 state
作为参数传递进来。这个 state
通常是由 __getstate__()
返回的状态。
何时使用:如果你想要自定义对象在反序列化时的重建过程,或者需要重新设置某些属性,可以使用 __setstate__()
方法。
destruct() XD
1 | class MyClass: |
在这个例子中,__setstate__()
方法在对象反序列化时恢复对象状态,并重新赋值 secret
。
__getnewargs__(self)
功能:这个方法允许你为对象的反序列化返回一个用于 __new__()
的参数元组。它控制对象在反序列化时,传递给 __new__()
方法的参数。
何时使用:如果你的类需要通过 __new__()
来创建对象,并且这个对象初始化时需要特定的参数(而不仅仅是 __init__()
),你可以使用 __getnewargs__()
。
1 | class MyClass: |
__getnewargs_ex__(self)
- 功能:类似于
__getnewargs__()
,但它可以返回一个包含位置参数和关键字参数的元组,分别传递给__new__()
方法。 - 何时使用:当你希望在对象反序列化时,既能传递位置参数,又能传递关键字参数时,使用
__getnewargs_ex__()
方法。
1 | class MyClass: |
__new__(cls, *args, **kwargs)
功能:__new__()
是用于创建类实例的构造器方法,它在对象创建时会被首先调用。pickle
在反序列化时有时会调用 __new__()
来创建新的对象,而不是通过 __init__()
。
何时使用:当类的实例需要自定义的创建行为时,__new__()
非常有用,特别是当对象需要被反序列化时,必须通过特殊的方式创建。
__new__()
是用来创建对象的静态方法,它在对象初始化之前被调用。__new__()
方法负责分配内存,并返回一个新的对象实例。在 Python 中,__new__()
通常只用于不可变对象(如 int
、str
、tuple
),因为这些对象在创建后不能被修改,必须通过 __new__()
返回新的实例。
详解:
1 | class MyClass: |
详解__new__()
首先明确一点:
在 Python 中,类的实例化分为两步:创建对象 和 初始化对象。__new__()
和 __init__()
分别负责这两部分。
1 | class MyClass: |
例子一目了然
1 | import pickle |
我们在 __new__()
方法中创建了 Point
对象,并设置了 _x
和 _y
属性。这样,__init__()
就不会修改这些属性,确保对象是不可变的(虽然在 Python 中的严格不可变性需要更多的措施,比如通过 __slots__
限制修改,但此处已展示核心思想)。
虽然定义了
__init__()
,但它没有实际做任何操作,因为对象的属性已经在__new__()
中设置好了。这样可以避免初始化时再次修改属性。
**__getnewargs__()
**:
__getnewargs__()
返回一个包含_x
和_y
的元组,表示反序列化时,应该将这些参数传递给__new__()
,以便重新创建对象。- 这个方法在
pickle
反序列化时被调用,用于确保__new__()
能够接收到正确的参数并重建对象。
对象在正常使用过程中是不可变的,但通过特殊的机制(如 pickle
的反序列化),对象的内部状态可以被“重新构建”或“重设”。这是因为 pickle
在反序列化时调用了 __new__()
和 __getnewargs__()
等特殊方法,允许对象被重新创建。
改一下
1 | import pickle |
关键:
不可变对象设计:通过
__new__()
和属性封装的方式,创建了一个在常规使用中不可修改属性的对象。反序列化的特殊性:
pickle
通过__getnewargs__()
等特殊方法,可以在反序列化时给对象不同的初始参数,允许重新设置对象的属性。尽管对象被设计为不可变,但通过pickle
序列化和反序列化,我们仍然可以创建带有不同状态的对象。
覆盖哦
这里已经核心操作了
ModifiedPoint(Point)
pickle
的 R
操作码
- 在
pickle
的 opcode 中,R
与object.__reduce__()
紧密关联。R
操作码在反序列化时会从栈中取出两个值,第一个作为要调用的函数,第二个是传递给函数的参数元组,然后调用该函数,返回值作为反序列化后的对象。 R
操作符的存在允许我们手动指定一些反序列化行为,使得pickle
可以解析一些由object.__reduce__()
生成的复杂对象。
pickle过程详细解读
- pickle解析依靠Pickle Virtual Machine (PVM)进行。
- PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存:
- 解析引擎:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到
.
停止。最终留在栈顶的值将被作为反序列化对象返回。 - 栈:由Python的list实现,被用来临时存储数据、参数以及对象。
- memo:由Python的dict实现,为PVM的生命周期提供存储。说人话:将反序列化完成的数据以
key-value
的形式储存在memo中,以便后来使用。
- PVM解析
__reduce__()
的过程动图:
opcode最好用0版本的
1 | import pickle |
- pickle3版本的opcode示例:
1 | # 'abcd' |
pickletools
- 使用pickletools可以方便的将opcode转化为便于肉眼读取的形式
1 | import pickletools |
漏洞
利用
- 任意代码执行或命令执行。
- 变量覆盖,通过覆盖一些凭证达到绕过身份验证的目的。
初步认识:pickle EXP的简单demo
1 | import pickle |
- 变量覆盖
1 | import pickle |
如何手写opcode
- 在CTF中,很多时候需要一次执行多个函数或一次进行多个指令,此时就不能光用
__reduce__
来解决问题(reduce一次只能执行一个函数,当exec被禁用时,就不能一次执行多条指令了),而需要手动拼接或构造opcode了。手写opcode是pickle反序列化比较难的地方。 - 在这里可以体会到为何pickle是一种语言,直接编写的opcode灵活性比使用pickle序列化生成的代码更高,只要符合pickle语法,就可以进行变量覆盖、函数执行等操作。
- 根据前文不同版本的opcode可以看出,版本0的opcode更方便阅读,所以手动编写时,一般选用版本0的opcode。下文中,所有opcode为版本0的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时要想象栈中的数据,以正确使用每种opcode。
- 在理解时注意与python本身的操作对照(比如python列表的
append
对应a
、extend
对应e
;字典的update
对应u
)。 c
操作符会尝试import
库,所以在pickle.loads
时不需要漏洞代码中先引入系统库。- pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如
getattr
、dict.get
)才能进行。但是因为存在s
、u
、b
操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有c
、i
。而如何查值也是CTF的一个重要考点。 s
、u
、b
操作符可以构造并赋值原来没有的属性、键值对。
拼接opcode
将第一个pickle流结尾表示结束的 .
去掉,将第二个pickle流与第一个拼接起来即可。
About this Post
This post is written by void2eye, licensed under CC BY-NC 4.0.