Pickle反序列化
Pickle反序列化
上接ISCC2025校赛的有一道疑似SQL的题目被骗说是Pickle反序列化,但刚好也来补习一下
CTF题型 Python中pickle反序列化进阶利用&;例题&;opache绕过_python pickle ctf-CSDN博客
CTF-python pickle反序列化 - sijidou - 博客园
简介
pickle是Python的一个库,可以对一个对象进行序列化和反序列化操作.其中__reduce__
魔法函数会在一个对象被反序列化时自动执行,我们可以通过在__reduce__
魔法函数内植入恶意代码的方式进行任意命令执行.通常会利用到Python的反弹shell.
前置知识
python对象
在python中,对象的概念十分广泛.
对象是数据和功能的结合体。Python是一种面向对象编程语言,它使用对象来组织代码和数据。在Python中,几乎所有的东西都是对象,包括整数、浮点数、列表、元组、字典、函数、类等。
一个Python对象通常包含以下部分:
- 身份(Identity):每个对象都有一个唯一的身份标识,通常是它的内存地址。可以使用内建函数
id()
来获取对象的身份 - 类型(Type):对象属于某种类型,比如整数、浮点数、字符串、列表等。可以使用内建函数
type()
来获取对象的类型 - 值(Value):对象所持有的数据。不同类型的对象有不同的值。例如,整数对象的值是整数值,字符串对象的值是字符序列
- 属性(Attributes):对象可以有零个或多个属性,这些属性是附加到对象上的数据。属性通常用于存储对象的状态信息
- 方法(Methods):对象可以有零个或多个方法,方法是附加到对象上的函数。这些方法定义了对象可以执行的操作
Python面向对象
python是一门面向对象的语言.也正因为python面向对象的特性,使得我们有更加丰富的选择进行绕过
在Python中,面向对象的思想和php是一致的,只是定义类的代码,调用类函数和类属性的方式和php不同
python中用.
调用实例的属性和方法
python中存在类属性和实例属性,实例属性只对一个实例生效,类属性对一个类生效.定义实例属性的方法是用__init__
魔术方法.调用类属性的方法是类名.变量名
或者self.__class__.变量名
同样地,python的面向对象也有私有属性,私有方法,类的继承等
序列化和反序列化
序列化就是将一个对象转换为以字符串方式存储的过程,反序列化就是将字符串重新变为一个对象的实例
注意,在linux下和windows下进行序列化的操作的结果可能会有所不同,在做题时需要根据靶机的系统选择用windows还是linux进行序列化操作.
关于序列化和反序列化的函数
pickle.dump()
pickle.load()
pickle.dumps()
pickle.loads()
其中两个dump函数是把python对象转换为二进制对象的,两个load函数是把二进制对象转换为python对象的
而s函数是指对字符串进行反序列化和序列化操作,另外两个函数是对文件进行操作
python魔术方法
和php类似,python魔术方法也会在一些特定情况下被自动调用.我们尤其要注意的是__reduce__
魔术方法,这会在反序列化过程开始时被调用,所以我们可以序列化一个__reduce__
魔术方法中有系统命令的实例并且让服务器将它反序列化,从而达到任意命令执行的效果
除此之外还有很多魔术方法.例如初始化函数__init__
和构造函数__new__
.和php类似,python中也有魔法属性.例如__doc__
,__name__
,__class__
,__base__
等
pickle.loads()
会在反序列化一个实例时自动引入没有引入的库
构造方法__new__
- 在实例化一个类时自动被调用,是类的构造方法
- 可以通过重写
__new__
自定义类的实例化过程
初始化方法__init__
- 在
__new__
方法之后被调用,主要负责定义类的属性,以初始化实例
析构方法__del__
- 在实例将被销毁时调用
- 只在实例的所有调用结束后才会被调用
1 | __getattr__ |
- 获取不存在的对象属性时被触发
- 存在返回值
1 | __setattr__ |
- 设置对象成员值的时候触发
- 传入一个self,一个要设置的属性名称,一个属性的值
1 | __repr__ |
- 在实例被传入
repr()
时被调用 - 必须返回字符串
1 | __call__ |
- 把对象当作函数调用时触发
1 | __len__ |
- 被传入
len()
时调用 - 返回一个整型
1 | __str__ |
- 被
str()
,format()
,print()
调用时调用,返回一个字符串
栈
栈是一种存储数据的结构.栈有压栈和弹栈两种操作
可以把栈看做一个弹夹,先进栈的数据后出栈,压栈就像压子弹,弹栈就像弹子弹
什么是PVM
pickle是一种栈语言,它由一串串opcode(指令集)组成.该语言的解析是依靠Pickle Virtual Machine (PVM)进行的
为什么要学习pickle?
pickle实际上可以看作一种独立的语言,通过对
opcode
的编写可以进行Python代码执行、覆盖变量等操作。直接编写的opcode
灵活性比使用pickle序列化生成的代码更高,并且有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。
PVM由以下三部分组成
- 指令处理器:从流中读取
opcode
和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回 - stack:由 Python 的
list
实现,被用来临时存储数据、参数以及对象 - memo:由 Python 的
dict
实现,为 PVM 的整个生命周期提供存储
语法
操作码 | 描述 | 具体写法 | 栈上的变化 | memo 上的变化 |
---|---|---|---|---|
c |
获取一个全局对象或导入一个模块 | 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 字符串对象 | Vxxxn |
获得的对象入栈 | 无 |
I |
实例化一个 int 对象 | Ixxxn |
获得的对象入栈 | 无 |
F |
实例化一个 float 对象 | Fx.xn |
获得的对象入栈 | 无 |
R |
选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 | R |
函数和参数出栈,函数的返回值入栈 | 无 |
. |
程序结束,栈顶的一个元素作为 pickle.loads() 的返回值 |
. |
无 | 无 |
( |
向栈中压入一个 MARK 标记 | ( |
MARK 标记入栈 | 无 |
t |
寻找栈中的上一个 MARK,并组合之间的数据为元组 | t |
MARK 标记以及被组合的数据出栈,获得的对象入栈 | 无 |
) |
向栈中直接压入一个空元组 | ) |
空元组入栈 | 无 |
l |
寻找栈中的上一个 MARK,并组合之间的数据为列表 | l |
MARK 标记以及被组合的数据出栈,获得的对象入栈 | 无 |
\] |
向栈中直接压入一个空列表 | \] |
空列表入栈 | 无 |
d |
寻找栈中的上一个 MARK,并组合之间的数据为字典(数据必须有偶数个,即呈 key-value 对) | d |
MARK 标记以及被组合的数据出栈,获得的对象入栈 | 无 |
} |
向栈中直接压入一个空字典 | } |
空字典入栈 | 无 |
p |
将栈顶对象储存至 memo_n | pnn |
无 | 对象被储存 |
g |
将 memo_n 的对象压栈 | gnn |
对象被压栈 | 无 |
0 |
丢弃栈顶对象 | 0 |
栈顶对象被丢弃 | 无 |
b |
使用栈中的第一个元素(储存多个属性名:属性值的字典)对第二个元素(对象实例)进行属性设置 | b |
栈上第一个元素出栈 | 无 |
还有h0ckser师傅的指令集:
1 | # Pickle opcodes. See pickletools.py for extensive docs. The listing |
例如:
1 | payload = b'''(cos |
解释:
(
:向栈中压入一个 MARK 标记cos
:导入os
模块system
:获取os.system
函数并压栈S'cat /f* > /tmp/a'
:将字符串'cat /f* > /tmp/a'
压栈o.
:寻找栈中的上一个 MARK,以之间的第一个数据(os.system
函数)为 callable,第二个数据(字符串)为参数,执行该函数。.
表示程序结束,栈顶的一个元素作为pickle.loads()
的返回值
1:栈的初始状态
1 | 栈底 |
2:导入 os
模块
1 | 栈底 |
3:获取 os.system
函数
1 | 栈底 |
4:压入命令字符串
1 | 栈底 |
5:调用 os.system
函数
1 | 栈底 |
执行 o
操作码后,os.system
函数被调用,参数为 'cat /f* > /tmp/a'
。栈中的 MARK
、os模块
、os.system函数
和命令字符串都被弹出,函数的返回值(如果有的话)会被压入栈
6:结束序列化
1 | 栈底 |
执行 .
操作码后,序列化过程结束,栈顶的值(None)作为 pickle.loads()
的返回值
也可以使用以下代码片段来生成Pickle序列化:
1 | import pickle |
也可以将Pickle的代码变得可读起来(并非好读):
1 | import pickletools |
db.
:
d
操作码:从栈中弹出键值对('secret'
和'Hack!!!'
),构建一个字典{'secret': 'Hack!!!'}
,并将其压入栈b
操作码:弹出栈顶的字典和前面的对象(假设是通过c__main__
导入的secret
对象),将字典中的属性绑定到对象上。即为secret
对象添加一个属性'secret'
,其值为'Hack!!!'
.
操作码:结束反序列化过程
pker的使用
1
利用
覆盖变量
比如:
1 | import pickletools |
输出星穹铁道即代表成功覆盖了变量
1 | opcode=b"""c__main__ |
RCE
相关的就是 c
,R
,o
,i
,b
这几个操作符
与函数执行相关的opcode有三个: R
、 i
、 o
,所以我们可以从三个方向进行构造:
R
:(R
操作符用于构建对象)
1 | b'''cos |
tR
:其中 t
表示元组(tuple),R
表示调用 os.system
函数,并将字符串 'whoami'
作为参数传递给它
1 | cos |
i
:
1 | b'''(S'whoami' |
i
像是o
和c
的结合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
o
:
1 | b'''(cos |
实例化对象
1 | import pickle |