SSTI模板注入
前言
开局一张图,姿势全靠yy
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。与此同时,它也扩展了黑客的攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE(远程代码执行)。通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,攻击者依然有许多手段绕过它。
和常见Web注入的成因一样,也是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。
小课堂---模板渲染是what?
首先 模板渲染分解为前端渲染和后端渲染,还有浏览器渲染。
模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。
通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。
后端渲染
后端渲染HTML的情况下,浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串,这里的计算就是服务器经过解析存放在服务器端的模板文件来完成的,在这种情况下,浏览器只进行了HTML的解析,以及通过操作系统提供的操纵显示器显示内容的系统调用在显示器上把HTML所代表的图像显示给用户。
实现:后端拼字符串呗…… (理论上后端模板也是字符串)
好处:模板统一在后端。前端(相对)省事,不占用客户端运算资源(解析模板),只要不大改结构,文字啥的小修改后端改了就好了。
坏处:占用(部分、少部分)服务器运算资源、,response 出的数据量会(稍)大点,模板改了前端的交互和样式什么的一样得跟着联动修改。
前端渲染
前端渲染就是指浏览器会从后端得到一些信息,这些信息或许是适用于题主所说的angularjs的模板文件,亦或是JSON等各种数据交换格式所包装的数据,甚至是直接的合法的HTML字符串。这些形式都不重要,重要的是,将这些信息组织排列形成最终可读的HTML字符串是由浏览器来完成的,在形成了HTML字符串之后,再进行显示。
好处:不占用服务端运算资源(解析模板),模板在前端(很有可能仅部分在前端),改结构变交互都前端自己来了,改完自己调就行。不用麻烦后端再联调神马的。
坏处:占用(一部分、少部分)客户端运算资源(解析模板)。前端代码多点,毕竟包含模板代码了么。脚本是不是首次下就慢点了(看你在意不在意这个毕竟能304和CDN啥的)。可能造成前后两份模板的情况,总归要后端吐出个首屏啥的先让用户看见吧。那这部分页面模板不就是后端拼好了吐出来的么。
示例1:定义一个模板,例如
<html>
<div>{$what}</div>
</html>
这只是一个模板。{$what}是数据。此时不知道数据是什么。
如果我想html里面成为
<div>peiqi</div>
渲染到html代码里
渲染完成后
<html>
<div>peiqi</div>
</html>
当然这只是最简答的例子;
一般来说,至少会提供分支,迭代。还有一些内置函数,如格式化等等
那么 {$what}这个数据如何代码传入。
比如我工作用的模板引擎是smarty
我定义了一个模板。
<html>
<div>{$what}</div>
</html>
js代码就是这么写就可以了
var tpl= new jSmart(tplStr);//tplStr就是模板的字符串。
var content = "Hello World";
tpl.fetch({what:content});
这样就可以了
其余的就交给引擎去渲染执行了。
示例2:
模板:front.tpl
<div>
{%$a%}
</div>
后端:
设置变量:$smarty->assign('a', 'give data');
展示模板:$smarty->display("front.tpl");
到前端时是渲染好的html串:
<div>
give data
</div>
这种方式的特点是展示数据快,直接后端拼装好数据与模板,展现到用户面前。
为什么需要服务器模板
首先解释一下为什么HTML代码和应用程序逻辑混合在一起不好,看下面的例子你就知道了。假如你使用下面的代码为你的用户提供服务:
这不仅仅是静态HTML代码。用户名是从cookie里获取并且自动填写的。这样一来,只要你之前登录过该网站,你就无需再次输入。但是这有一个问题,那就是你必须通过某种形式将值插入到HTML文档中,有两种方法可以实现,一种是正确的,一种是有危害的。不过我们要先问一下,为什么要这么做。
下图展示了解决该问题的完全错误的方法:
这段代码有很多问题,作者不仅仅没有对用户的输入进行处理,HTML代码和PHP代码复杂的混合在一起,非常难以理解。部分HTML代码分布在多个函数中,这还不算什么,当你尝试修改HTML代码中任何内容时,你才发现有多难受,比如新增css类或修改HTML标签的顺序。
上面这个例子显然是有意写的这么烂的,不过可以通过格式化进行优化。然而,在大型的代码库中,即使代码格式良好,也会很快变得无法管理。这就是为什么我们需要模板。
与上面混乱的代码相比,服务器端模板提供了一种更加简单的方法来管理动态生成的HTML代码。最大的优点就是你可以在服务器端动态生成HTML页面,看起来跟静态HTML页面一样。现在我们来看看,当我们使用服务器端模板时,复杂的代码看起来如何。
这对前面的代码做了一些优化,它依然是静态的。为了显示正确的信息而不是大括号占位符,我们需要一个替换它们的模板引擎。后端代码可能是这样的。
这段代码的意思非常清楚,首先加载login.tpl模板文件,然后对与模板中名称相同的变量赋值(大括号里的变量),然后调用show()函数,相应的替换它们的内容并输出HTML代码。然而,我们在模板中增加了新的功能,这将会向用户展示模板渲染的时间。
什么是服务端模板注入
通过模板,Web应用可以把输入转换成特定的HTML文件或者email格式。就拿一个销售软件来说,我们假设它会发送大量的邮件给客户,并在每封邮件前SKE插入问候语,它会通过Twig(一个模板引擎)做如下处理:
$output = $twig->render( $_GET['custom_email'] , array("first_name" => $user.first_name) );
有经验的读者可能迅速发现 XSS,但是问题不止如此。这行代码其实有更深层次的隐患,假设我们发送如下请求:
custom_email={{7*7}} // GET 参数
49 // $output 结果
还有更神奇的结果:
custom_email={{self}} // GET 参数
Object of class
__TwigTemplate_7ae62e582f8a35e5ea6cc639800ecf15b96c0d6f78db3538221c1145580ca4a5
could not be converted to string // 错误
我们不难猜到服务器执行了我们传过去的数据。每当服务器用模板引擎解析用户的输入时,这类问题都有可能发生。除了常规的输入外,攻击者还可以通过 LFI(文件包含)触发它。模板注入和 SQL 注入的产生原因有几分相似——都是将未过滤的数据传给引擎解析。
为什么我们在模板注入前加“服务端”呢?这是为了和 jQuery,KnockoutJS 产生的客户端模板注入区别开来。通常的来讲,前者甚至可以让攻击者执行任意代码,而后者只能 XSS。
注入原理
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {{name}}", array("name" => $_GET["name"])); // 将用户输入作为模版变量的值
echo $output;
?>
使用 Twig 模版引擎渲染页面,其中模版含有 {{name}} 变量,其模版变量值来自于 GET 请求参数 $_GET["name"]。
显然这段代码并没有什么问题,即使你想通过 name 参数传递一段 JavaScript 代码给服务端进行渲染,也许你会认为这里可以进行 XSS,
但是由于模版引擎一般都默认对渲染的变量值进行编码和转义,所以并不会造成跨站脚本攻击:
但是,如果渲染的模版内容受到用户的控制,情况就不一样了。修改代码为:
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET['name']}"); // 将用户输入作为模版内容的一部分
echo $output;
?>
上面这段代码在构建模版时,拼接了用户输入作为模板的内容,现在如果再向服务端直接传递 JavaScript 代码,用户输入会原样输出,测试结果显而易见。
在 Twig 模板引擎里, {{var}} 除了可以输出传递的变量以外,还能执行一些基本的表达式然后将其结果作为该模板变量的值,例如这里用户输入 name={{2*10}} ,则在服务端拼接的模版内容为:
这里简单分析一下,由于 {# comment #} 作为 Twig 模板引擎的默认注释形式,所以在前端输出的时候并不会显示,而 {{2*8}} 作为模板变量最终会返回 16 作为其值进行显示,因此前端最终会返回内容 Hello IsVuln16OK ,如下图:
漏洞发掘
每一个(重)模板引擎都有着自己的语法(点),Payload 的构造需要针对各类模板引擎制定其不同的扫描规则,就如同 SQL 注入中有着不同的数据库类型一样。
简单来说,就是更改请求参数使之承载含有模板引擎语法的 Payload,通过页面渲染返回的内容检测承载的 Payload 是否有得到编译解析,有解析则可以判定含有 Payload 对应模板引擎注入,否则不存在 SSTI。
模板语言的语法和 HTML 语法相差甚大,因此我们可以用其独特的语法来探测漏洞。虽然各种模板的实现细节不大一样,不过它们的基本语法大致相同,我们可以发送如下 payload:
smarty=Hello ${7*7}
Hello 49
freemarker=Hello ${7*7}
Hello 49
来确认漏洞。
在一些环境下,用户的输入也会被当作模板的可执行代码。比如说变量名:
personal_greeting=username
Hello user01
这种情况下,XSS 的方法就无效了。但是我们可以通过破坏 template 语句,并附加注入的HTML标签以确认漏洞:
personal_greeting=username<tag>
Hello
personal_greeting=username}}<tag>
Hello user01 <tag>
但是,如果渲染的模版内容受到用户的控制,情况就不一样了。修改代码为:
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET['name']}"); // 将用户输入作为模版内容的一部分
echo $output;
?>
上面这段代码在构建模版时,拼接了用户输入作为模板的内容,现在如果再向服务端直接传递 JavaScript 代码,用户输入会原样输出,测试结果显而易见。
模板引擎注入
模板引擎
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
一些模板引擎:Smarty,Mako,Jinja2,Jade,Velocity,Freemaker和Twig,模板注入是一种注入攻击,可以产生一些特别有趣的影响。对于AngularJS的情况,这可能意味着XSS,并且在服务器端注入的情况下可能意味着远程代码执行。
重点来了,不同引擎有不同的测试以及注入方式!
flask/jinja2模板注入
Flask是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2
Flask框架中提供的模版引擎可能会被一些无量开发者利用引入一个服务端模版注入漏洞,如果对此感到有些困惑可以看看James Kettle在黑帽大会中分享的议题(PDF),简而言之这个漏洞允许将语言/语法注入到模板中。在服务器的context中执行这个输入重现,根据应用的context可能导致任意远程代码执行(远端控制设备)
参考文章:https://www.freebuf.com/articles/web/88768.html
参考文章:https://www.freebuf.com/articles/web/98928.html
PHP/模版引擎Twig注入
可以参考本博客文章 Flask从零到无 。
这里给出一个漏洞环境代码,本地测试
from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string
app = Flask(__name__)
@app.route('/test',methods=['GET', 'POST'])
def test():
template = '''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
return render_template_string(template)
if __name__ == '__main__':
app.debug = True
app.run()
代码简析: 我们自己简单写一个string类型的 html,html返回当前url,我们放入到渲染函数render_template_string进行渲染,然后页面会打印出当前url,如果url里含有{{}} 那么便可以进行模板注入。
测试url http://127.0.0.1:5000/test?{{config}}
测试结果如下:
而如果我们使用render_template函数,
@app.route('/',methods=['GET', 'POST'])
@app.route('/index',methods=['GET', 'POST'])#我们访问/或者/index都会跳转
def index():
return render_template("index.html",title='Home',user=request.args.get("key"))
index.html
<html>
<head>
<title>{{title}} - 小猪佩奇</title>
</head>
<body>
<h1>Hello, {{user}}!</h1>
</body>
</html>
那么将不会有模板注入,因为render_template已经传入一个固定好了的模板,没法再去修改,在渲染之后传入数据,只有当第一种代码,我们模板可控的时候,先传入后渲染,这样才会导致ssti模板注入。
参考:https://www.freebuf.com/vuls/83999.html
CTF---模板注入
tornado的一道 ssti
https://blog.csdn.net/cccccfive/article/details/83145669
防御
为了防止此类漏洞,你应该像使用eval()函数一样处理字符串加载功能。尽可能加载静态模板文件。
注意:我们已经确定此功能类似于require()函数调用。因此,你也应该防止本地文件包含(LFI)漏洞。不要允许用户控制此类文件或其内容的路径。
另外,无论在何时,如果需要将动态数据传递给模板,不要直接在模板文件中执行,你可以使用模板引擎的内置功能来扩展表达式,实现同样的效果。