逃逸安全的模板沙箱(一)——FreeMarker(上)

本文首发于Seebug Paper,原文链接:https://paper.seebug.org/1304/

前言

8月5日 @pwntester 联合 @Oleksandr Mirosh 发表了一个关于 Java 模板注入的BlackHat USA 2020 议题[1],议题介绍了现阶段各种 CMS 模板引擎中存在的缺陷,其中包含通用缺陷以及各个模板引擎特性造成的缺陷。由于不同模板引擎有不同语法特性,因此文章将分为系列文章进行阐述。

笔者前期主要是对 Liferay 的 FreeMarker 引擎进行了调试分析,故本文先以 FreeMarker 为例,梳理该模板引擎 SSTI 漏洞的前世今生,同时叙述自己的 Liferay FreeMarker SSTI 漏洞踩坑历程及对 Liferay 安全机制的分析。由于涉及内容比较多,请大家耐心阅读,若是已经本身对 FreeMarker 引擎有了解,可直接跳到文章后半部分阅读。

FreeMarker基础知识

FreeMarker 是一款模板引擎,即一种基于模板和需要改变的数据, 并用来生成输出文本( HTML 网页,电子邮件,配置文件,源代码等)的通用工具,其模板语言为 FreeMarker Template Language (FTL)。

image-20200807155408983

在这里简单介绍下 FreeMarker 的几个语法,其余语法指令可自行在 FreeMarker 官方手册[2]进行查询。

FTL指令规则

在 FreeMarker 中,我们可以通过FTL标签来使用指令。FreeMarker 有3种 FTL 标签,这和 HTML 标签是完全类似的。

开始标签:<#directivename parameter> 
结束标签:</#directivename> 
空标签:<#directivename parameter/> 

实际上,使用标签时前面的符号 # 也可能变成 @,如果该指令是一个用户指令而不是系统内建指令时,应将 # 符号改成 @ 符号。这里主要介绍 assign 指令,主要是用于为该模板页面创建替换一个顶层变量。

<#assign name1=value1 name2=value2 ... nameN=valueN>
or
<#assign same as above... in namespacehash>
or
<#assign name>
  capture this
</#assign>
or
<#assign name in namespacehash>
  capture this
</#assign>
    
Tips:name为变量名,value为表达式,namespacehash是命名空间创建的哈希表,是表达式。
    
for example:
<#assign seq = ["foo", "bar", "baz"]>//创建了一个变量名为seq的序列

创建好的变量,可以通过插值进行调用。插值是用来给表达式插入具体值然后转换为文本(字符串),FreeMarker 的插值主要有如下两种类型:

  • 通用插值:${expr}
  • 数字格式化插值: #{expr}

这里主要介绍通用插值,当插入的值为字符串时,将直接输出表达式结果,举个例子:

eg:
${100 + 5} => 105
${seq[1]} => bar //上文创建的序列

插值仅仅可以在两种位置使用:在文本区(比如 Hello ${name}!) 和字符串表达式(比如 <#include "/footer/${company}.html">)中。

内建函数

FreeMarker 提供了大量的内建函数,用于拓展模板语言的功能,大大增强了模板语言的可操作性。具体用法为 variable_name?method_name。然而其中也存在着一些危险的内建函数,这些函数也可以在官方文档中找到,此处不过多阐述。主要介绍两个内建函数,apinew,如果开发人员不加以限制,将造成极大危害。

  • api函数

    如果 value 本身支撑api这个特性,value?api会提供访问 value 的 API(通常为 Java API),比如value?api.someJavaMethod()

    eg:
    <#assign classLoader=object?api.class.protectionDomain.classLoader>
    //获取到classloader即可通过loadClass方法加载恶意类
    

    但值得庆幸的是,api内建函数并不能随意使用,必须在配置项api_builtin_enabledtrue时才有效,而该配置在2.3.22版本之后默认为false

  • new函数

    这是用来创建一个具体实现了TemplateModel接口的变量的内建函数。在 ? 的左边可以指定一个字符串, 其值为具体实现了 TemplateModel 接口的完整类名,然后函数将会调用该类的构造方法生成一个对象并返回。

    //freemarker.template.utility.Execute实现了TemplateMethodModel接口(继承自TemplateModel)
    <#assign ex="freemarker.template.utility.Execute"?new()> 
        ${ex("id")}//系统执行id命令并返回
    => uid=81(tomcat) gid=81(tomcat) groups=81(tomcat)
    

    拥有编辑模板权限的用户可以创建任意实现了 TemplateModel 接口的Java对象,同时还可以触发没有实现 TemplateModel 接口的类的静态初始化块,因此new函数存在很大的安全隐患。好在官方也提供了限制的方法,可以使用 Configuration.setNewBuiltinClassResolver(TemplateClassResolver) 或设置 new_builtin_class_resolver 来限制这个内建函数对类的访问(从 2.3.17版开始)。

FreeMarker初代SSTI漏洞及安全机制

经过前文的介绍,我们可以发现 FreeMarker 的一些特性将造成模板注入问题,在这里主要通过apinew两个内建函数进行分析。

  • api 内建函数的利用

    我们可以通过api内建函数获取类的classloader然后加载恶意类,或者通过Class.getResource的返回值来访问URI对象。URI对象包含toURLcreate方法,我们通过这两个方法创建任意URI,然后用toURL访问任意URL。

    eg1:
    <#assign classLoader=object?api.class.getClassLoader()>
    ${classLoader.loadClass("our.desired.class")}
        
    eg2:
    <#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>]
    
  • new 内建函数的利用

    主要是寻找实现了 TemplateModel 接口的可利用类来进行实例化。freemarker.template.utility包中存在三个符合条件的类,分别为Execute类、ObjectConstructor类、JythonRuntime类。

    <#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
    <#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc.exe").start()}
    <#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")</@value>//@value为自定义标签
    

当然对于这两种方式的利用,FreeMarker 也做了相应的安全措施。针对api的利用方式,设置配置项api_builtin_enabled的默认值为false。同时为了防御通过其他方式调用恶意方法,FreeMarker内置了一份危险方法名单unsafeMethods.properties[3],诸如getClassLoadernewInstance等危险方法都被禁用了,下面列出一小部分,其余请自行查阅文件。

//unsafeMethods.properties
java.lang.Object.wait()
java.lang.Object.wait(long)
java.lang.Object.wait(long,int)
java.lang.Object.notify()
java.lang.Object.notifyAll()

java.lang.Class.getClassLoader()
java.lang.Class.newInstance()
java.lang.Class.forName(java.lang.String)
java.lang.Class.forName(java.lang.String,boolean,java.lang.ClassLoader)

java.lang.reflect.Constructor.newInstance([Ljava.lang.Object;)
...
more

针对new的利用方式,上文已提到过官方提供的一种限制方式——使用 Configuration.setNewBuiltinClassResolver(TemplateClassResolver) 或设置 new_builtin_class_resolver 来限制这个内建函数对类的访问。此处官方提供了三个预定义的解析器:

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

当然用户自身也可以自定义解析器以拓展对危险类的限制,只需要实现TemplateClassResolver接口就好了,接下来会介绍到的 Liferay 就是通过其自定义的解析器LiferayTemplateClassResolver去构建 FreeMarker 的模板沙箱。

Liferay FreeMarker模板引擎SSTI漏洞踩坑历程

碰出一扇窗

在研究这个 BlackHat 议题的过程中,我们遇到了很多问题,接下来就顺着我们的分析思路,一起探讨 Liferay 的安全机制,本次测试用的环境为 Liferay Portal CE 7.3 GA1。

先来看看 GHSL 安全团队发布的 Liferay SSTI 漏洞通告[4]:

Even though Liferay does a good job extending the FreeMarker sandbox with a custom ObjectWrapper (com.liferay.portal.template.freemarker.internal.RestrictedLiferayObjectWrapper.java) which enhances which objects can be accessed from a Template, and also disables insecure defaults such as the ?new built-in to prevent instantiation of arbitrary classes, it stills exposes a number of objects through the Templating API that can be used to circumvent the sandbox and achieve remote code execution.

Deep inspection of the exposed objects' object graph allows an attacker to get access to objects that allow them to instantiate arbitrary Java objects.

可以看到,给出的信息十分精简有限,但是还是能从中找到关键点。结合议题介绍和其他同类型的漏洞介绍,我们能梳理出一些关键点。

  • Exposed Object

    通告中提及了通过模板 API 暴露出大量的可访问对象,而这些对象即为 SSTI 漏洞的入口,通过这些对象的方法或者属性可以进行模板沙箱的绕过。这也是议题的一大重点,因为大多数涉及第三方模板引擎的CMS都没有对这些暴露的对象进行控制。

  • RestrictedLiferayObjectWrapper.java

    根据介绍,该自定义的ObjectWrapper拓展了FreeMarker的安全沙箱,增强了可通过模板访问的对象,同时也限制了不安全的默认配置以防止实例化任何类,比如?new方法。可以看出这是Liferay赋予模板沙箱的主要安全机制。

可以看到,重点在于如何找到暴露出的对象,其次思考如何利用这些对象绕过Liferay的安全机制。

我们在编辑模板时,会看到一个代码提示框。列表中的变量都是可以访问的,且无需定义,也不用实现TemplateModel接口。但该列表会受到沙箱的限制,其中有一部分对象被封禁,无法被调用。

image-20200810180816309

这些便是通过模板 API 暴露出来的一部分对象,但这是以用户视角所看到的,要是我们以运行态的视角去观察呢。既然有了暴露点,其背后肯定存在着许多未暴露出的对象。

所以我们可以通过调试定位到一个关键对象——FreeMarkerTemplate,其本质上是一个Map<String, Object>对象。该对象不仅涵盖了上述列表中的对象,还存在着很多其他未暴露出的对象。整个FreeMarkerTemplate对象共列出了154个对象,大大拓宽了我们的利用思路。在FreeMarker引擎里,这些对象被称作为根数据模型(rootDataModel)。

image-20200811110240319

那么可以尝试从这154个对象中找出可利用的点,为此笔者进行了众多尝试,但由于 Liferay 健全的安全机制,全都失败了。下面是一些调试过程中发现在后续利用过程中可能有用的对象:

"getterUtil" -> {GetterUtil_IW@47242} //存在各种get方法
"saxReaderUtil" -> {$Proxy411@47240} "com.liferay.portal.xml.SAXReaderImpl@294e3d8d"
    //代理对象,存在read方法,可以传入File、url等参数
"expandoValueLocalService" -> {$Proxy58@47272} "com.liferay.portlet.expando.service.impl.ExpandoValueLocalServiceImpl@15152694"
    //代理对象,其handler为AopInvocationHandler,存在invoke方法,且方法名和参数名可控。proxy对象可以通过其setTarget方法进行替换。
"realUser" -> {UserImpl@49915}//敏感信息
"user" -> {UserImpl@49915}//敏感信息
"unicodeFormatter" -> {UnicodeFormatter_IW@47290} //编码转换
"urlCodec" -> {URLCodec_IW@47344} //url编解码
"jsonFactoryUtil" -> {JSONFactoryImpl@47260} //可以操作各种JSON相关方法

接下来将会通过叙述笔者对各种利用思路的尝试,对 Liferay 中 FreeMarker 模板引擎的安全机制进行深入分析。

“攻不破”的 Liferay FreeMarker 安全机制

在以往我们一般是通过Class.getClassloader().loadClass(xxx)的方式加载任意类,但是在前文提及的unsafeMethods.properties中,我们可以看到java.lang.Class.getClassLoader()方法是被禁止调用的。

这时候我们只能另辟蹊径,在 Java 官方文档中可以发现Class类有一个getProtectionDomain方法,可以返回一个ProtectionDomain对象[5]。而这个对象同时也有一个getClassLoader方法,并且ProtectionDomain.getClassLoader方法并没有被禁止调用。

获取CLassLoader的方式有了,接下来,我们只要能够获得class对象,就可以加载任意类。但是当我们试图去获取class对象时,会发现这是行不通的,因为这会触发 Liferay 的安全机制。

image-20200811160236287

定位到 GHSL 团队提及的com.liferay.portal.template.freemarker.internal.RestrictedLiferayObjectWrapper.java文件,可以发现模板对象会经过wrap方法修饰。

通过wrap(java.lang.Object obj)方法,用户可以传入一个Object对象,然后返回一个与之对应的TemplateModel对象,或者抛出异常。模板在语法解析的过程中会调用TemplateModel对象的get方法,而其中又会调用BeansWrapperinvokeMethod进行解析,最后会调用外部的wrap方法对获取到的对象进行包装。

image-20200820162548125

此处的getOuterIdentity即为TemplateModel对象指定的Wrapper。除了预定义的一些对象,其余默认使用RestrictedLiferayObjectWrapper进行解析。

回到RestrictedLiferayObjectWrapper,该包装类主要的继承关系为RestrictedLiferayObjectWrapper->LiferayObjectWrapper->DefaultObjectWrapper->BeansWrapper,在wrap的执行过程中会逐步调用父类的wrap方法,那么先来分析RestrictedLiferayObjectWrapperwrap方法。

image-20200811161729512

wrap方法中会先通过getClass()方法获得class对象,然后调用_checkClassIsRestricted方法,进行黑名单类的判定。

image-20200811160917074

此处_allowedClassNames_restrictedClasses_restrictedMethodNames是在com.liferay.portal.template.freemarker.configuration.FreeMarkerEngineConfiguration中被预先定义的黑白名单,其中_allowedClassNames默认为空。对比一下7.3.0-GA1和7.3.2-GA3内置的黑名单:

  • 7.3.0-GA1

    @Meta.AD(name = "allowed-classes", required = false)
    public String[] allowedClasses();
    
    @Meta.AD(
       deflt = "com.liferay.portal.json.jabsorb.serializer.LiferayJSONDeserializationWhitelist|java.lang.Class|java.lang.ClassLoader|java.lang.Compiler|java.lang.Package|java.lang.Process|java.lang.Runtime|java.lang.RuntimePermission|java.lang.SecurityManager|java.lang.System|java.lang.Thread|java.lang.ThreadGroup|java.lang.ThreadLocal",
       name = "restricted-classes", required = false
    )
    public String[] restrictedClasses();
    
    @Meta.AD(
       deflt = "com.liferay.portal.model.impl.CompanyImpl#getKey",
       name = "restricted-methods", required = false
    )
    public String[] restrictedMethods();
    
    @Meta.AD(
    	deflt = "httpUtilUnsafe|objectUtil|serviceLocator|staticFieldGetter|staticUtil|utilLocator",
    	name = "restricted-variables", required = false
    )
    public String[] restrictedVariables();
    
  • 7.3.2-GA3

    @Meta.AD(name = "allowed-classes", required = false)
    public String[] allowedClasses();
    
    @Meta.AD(
    	deflt = "com.ibm.*|com.liferay.portal.json.jabsorb.serializer.LiferayJSONDeserializationWhitelist|com.liferay.portal.spring.context.*|io.undertow.*|java.lang.Class|java.lang.ClassLoader|java.lang.Compiler|java.lang.Package|java.lang.Process|java.lang.Runtime|java.lang.RuntimePermission|java.lang.SecurityManager|java.lang.System|java.lang.Thread|java.lang.ThreadGroup|java.lang.ThreadLocal|org.apache.*|org.glassfish.*|org.jboss.*|org.springframework.*|org.wildfly.*|weblogic.*",
    	name = "restricted-classes", required = false
    )
    public String[] restrictedClasses();
    
    @Meta.AD(
    	deflt = "com.liferay.portal.model.impl.CompanyImpl#getKey",
    	name = "restricted-methods", required = false
    )
    public String[] restrictedMethods();
    
    @Meta.AD(
    	deflt = "httpUtilUnsafe|objectUtil|serviceLocator|staticFieldGetter|staticUtil|utilLocator",
    	name = "restricted-variables", required = false
    )
    public String[] restrictedVariables();
    

已修复的7.3.2版本增加了许多黑名单类,而这些黑名单类就是绕过沙箱的重点。如何利用这些黑名单中提及的类,进行模板沙箱的绕过,我们放在下篇文章进行阐述,这里暂不讨论。

我们可以发现java.lang.Class类已被拉黑,也就是说模板解析的过程中不能出现Class对象。但是,针对这种过滤方式,依旧存在绕过的可能性。

GHSL 安全团队在 JinJava 的 SSTI 漏洞通告提及到了一个利用方式:

JinJava does a great job preventing access to Class instances. It will prevent any access to a Class property or invocation of any methods returning a Class instance. However, it does not prevent Array or Map accesses returning a Class instance. Therefore, it should be possible to get an instance of Class if we find a method returning Class[] or Map<?, Class>.

image-20200811171508479

既然Class对象被封禁,那么我们可以考虑通过Class[]进行绕过,因为黑名单机制是通过getClass方法进行判断的,而[Ljava.lang.Class并不在黑名单内。另外,针对Map<?,Class>的利用方式主要是通过get方法获取到Class对象,而不是通过getClass方法,主要是用于拓展获得Class对象的途径。因为需要自行寻找符合条件的方法,所以这种方式仍然具有一定的局限性,但是相信这个 trick 在某些场景下的利用能够大放光彩。

经过一番搜寻,暂未在代码中寻找到合适的利用类,因此通过Class对象获取ClassLoader的思路宣告失败。此外,实质上ClassLoader也是被加入到黑名单中的。因此就算我们能从模板上下文中直接提取出ClassLoader对象,避免直接通过Class获取,也无法操控到ClassLoader对象。

既然加载任意类的思路已经被 Liferay 的安全机制防住,我们只能换个思路——寻找一些可被利用的恶意类或者危险方法。此处主要有两个思路,一个是通过new内建函数实例化恶意类,另外一个就是上文提及的JSONFactoryImpl对象

文章开头提到过三种利用方式,但是由于 Liferay 自定义解析器的存在,均无法再被利用。定位到com.liferay.portal.template.freemarker.internal.LiferayTemplateClassResolver这个类,重点关注其resolve方法。可以看见,在代码层直接封禁了ExecuteObjectConstructor的实例化,其次又进行了黑名单类的判定。此处restrictedClassNames跟上文所用的黑名单一致。

image-20200811180258891

这时候可能我们会想到,只要另外找一个实现TemplateModel 接口并且不在黑名单内的恶意类(比如JythonRuntime类)就可以成功绕过黑名单。然而 Liferay 的安全机制并没有这么简单,继续往下看。resolve后半部分进行了白名单校验,而这里的allowedClasseNames在配置里面默认为空,因此就算绕过了黑名单的限制,没有白名单的庇护也是无济于事。

image-20200811181023948

黑白名单的配合,直接宣告了new内建函数利用思路的惨败。不过,在这个过程中,我们还发现了一个有趣的东西。

假设我们拥有控制白名单的权限,但是对于JythonRuntime类的利用又有环境的限制,这时候只能寻找其他的利用类。在调试过程中,我们注意到一个类——com.liferay.portal.template.freemarker.internal.LiferayObjectConstructor这个类的结构跟ObjectConstructor极其相似,也同样拥有exec方法,且参数可控。加入白名单测试弹计算器命指令,可以正常执行。

image-20200811184412017

虽然此处受白名单限制,利用难度较高。但是从另外的角度来看,LiferayObjectConstructor可以说是ObjectConstructor的复制品,在某些场景下可能会起到关键作用。

回归正题,此时我们只剩下一条思路——JSONFactoryImpl对象。不难发现,这个对象拥有着一系列与JSON有关的方法,其中包括serializedeserialize方法。

重点关注其deserialize方法,因为我们可以控制传入的JSON字符串,从而反序列化出我们需要的对象。此处_jsonSerializerLiferayJSONSerializer对象(继承自JSONSerializer类)。

image-20200813103859054

跟进LiferayJSONSerializer父类的fromJSON方法,发现其中又调用了unmarshall方法。

image-20200820175314583

unmarshall方法中会调用getClassFromHint方法,不过该方法在子类被重写了。

image-20200813105953450

跟进LiferayJSONSerializer.getClassFromHint方法,方法中会先进行javaClass字段的判断,如果类不在白名单里就移除serializable字段里的值,然后放进map字段中,最后将类名更改为java.util.HashMap如果通过白名单校验,就会通过contextName字段的值去指定ClassLoader用于加载javaClass字段指定的类。最后在方法末尾会执行super.getClassFromHint(object),回调父类的getClassFromHint的方法。

image-20200813140849709

我们回到unmarshall方法,可以看到在方法末尾处会再次调用unmarshall方法,实质上这是一个递归解析 JSON 字符串的过程。这里有个getSerializer方法,主要是针对不同的class获取相应的序列器,这里不过多阐述。

image-20200813145730352

因为递归调用的因素,每次都会进行类名的白名单判定。而白名单在portal-impl.jar里的portal.properties被预先定义:

//Line 7227
json.deserialization.whitelist.class.names=\
	com.liferay.portal.kernel.cal.DayAndPosition,\
	com.liferay.portal.kernel.cal.Duration,\
	com.liferay.portal.kernel.cal.TZSRecurrence,\
	com.liferay.portal.kernel.messaging.Message,\
	com.liferay.portal.kernel.model.PortletPreferencesIds,\
	com.liferay.portal.kernel.security.auth.HttpPrincipal,\
	com.liferay.portal.kernel.service.permission.ModelPermissions,\
	com.liferay.portal.kernel.service.ServiceContext,\
	com.liferay.portal.kernel.util.GroupSubscriptionCheckSubscriptionSender,\
	com.liferay.portal.kernel.util.LongWrapper,\
	com.liferay.portal.kernel.util.SubscriptionSender,\
	java.util.GregorianCalendar,\
	java.util.Locale,\
	java.util.TimeZone,\
	sun.util.calendar.ZoneInfo

可以看到,白名单成功限制了用户通过 JSON 反序列化任意类的操作。虽然白名单类拥有一个register方法,可自定义添加白名单类。但 Liferay 也早已意识到这一点,为了防止该类被恶意操控,将com.liferay.portal.json.jabsorb.serializer.LiferayJSONDeserializationWhitelist添加进黑名单。

至此,利用思路在 Liferay 的安全机制下全部惨败。Liferay 健全的黑白名单机制,从根源上限制了大多数攻击思路的利用,可谓是“攻不破”的铜墙铁壁。但是,在众多安全研究人员的猛烈进攻下,该安全机制暴露出一个弱点。通过这个弱点可一举击破整个安全机制,从内部瓦解整个防线。而关于这个弱点的阐述及其利用,我们下一篇文章见。

References

[1] Room for Escape: Scribbling Outside the Lines of Template Security

https://www.blackhat.com/us-20/briefings/schedule/#room-for-escape-scribbling-outside-the-lines-of-template-security-20292

[2] FreeMarker Java Template Engine

https://freemarker.apache.org/

[3] FreeMarker unsafeMethods.properties

https://github.com/apache/freemarker/blob/2.3-gae/src/main/resources/freemarker/ext/beans/unsafeMethods.properties

[4] GHSL-2020-043: Server-side template injection in Liferay - CVE-2020-13445

https://securitylab.github.com/advutiliisories/GHSL-2020-043-liferay_ce

[5] ProtectionDomain (Java Platform SE 8 )

https://docs.oracle.com/javase/8/docs/api/index.html?java/security/ProtectionDomain.html

[6] In-depth Freemarker Template Injection

https://ackcent.com/blog/in-depth-freemarker-template-injection/

[7] FreeMarker模板注入实现远程命令执行

https://www.cnblogs.com/Eleven-Liu/p/12747908.html

posted @ 2020-09-09 16:24  DEADF1SH_CAT  阅读(343)  评论(0编辑  收藏  举报