逃逸安全的模板沙箱(一)——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
。然而其中也存在著一些危險的內建函式,這些函式也可以在官方文件中找到,此處不過多闡述。主要介紹兩個內建函式,api
和new
,如果開發人員不加以限制,將造成極大危害。
-
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_enabled
為true
時才有效,而該配置在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 的一些特性將造成模板注入問題,在這裡主要通過api
和new
兩個內建函式進行分析。
-
api 內建函式的利用
我們可以通過
api
內建函式獲取類的classloader
然後載入惡意類,或者通過Class.getResource
的返回值來訪問URI
物件。URI
物件包含toURL
和create
方法,我們通過這兩個方法建立任意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],諸如getClassLoader
、newInstance
等危險方法都被禁用了,下面列出一小部分,其餘請自行查閱檔案。
//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:和第一個類似,但禁止解析
ObjectConstructor
,Execute
和freemarker.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
方法,而其中又會呼叫BeansWrapper
的invokeMethod
進行解析,最後會呼叫外部的wrap
方法對獲取到的物件進行包裝。
此處的getOuterIdentity
即為TemplateModel
物件指定的Wrapper
。除了預定義的一些物件,其餘預設使用RestrictedLiferayObjectWrapper
進行解析。
回到RestrictedLiferayObjectWrapper
,該包裝類主要的繼承關係為RestrictedLiferayObjectWrapper->LiferayObjectWrapper->DefaultObjectWrapper->BeansWrapper
,在wrap
的執行過程中會逐步呼叫父類的wrap
方法,那麼先來分析RestrictedLiferayObjectWrapper
的wrap
方法。
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 aClass
property or invocation of any methods returning aClass
instance. However, it does not prevent Array or Map accesses returning aClass
instance. Therefore, it should be possible to get an instance ofClass
if we find a method returningClass[]
orMap<?, 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
方法。可以看見,在程式碼層直接封禁了Execute
和ObjectConstructor
的例項化,其次又進行了黑名單類的判定。此處restrictedClassNames
跟上文所用的黑名單一致。
這時候可能我們會想到,只要另外找一個實現TemplateModel
介面並且不在黑名單內的惡意類(比如JythonRuntime
類)就可以成功繞過黑名單。然而 Liferay 的安全機制並沒有這麼簡單,繼續往下看。resolve
後半部分進行了白名單校驗,而這裡的allowedClasseNames
在配置裡面預設為空,因此就算繞過了黑名單的限制,沒有白名單的庇護也是無濟於事。
黑白名單的配合,直接宣告了new
內建函式利用思路的慘敗。不過,在這個過程中,我們還發現了一個有趣的東西。
假設我們擁有控制白名單的許可權,但是對於JythonRuntime
類的利用又有環境的限制,這時候只能尋找其他的利用類。在除錯過程中,我們注意到一個類——com.liferay.portal.template.freemarker.internal.LiferayObjectConstructor
,這個類的結構跟ObjectConstructor
極其相似,也同樣擁有exec
方法,且引數可控。加入白名單測試彈計算器命指令,可以正常執行。
雖然此處受白名單限制,利用難度較高。但是從另外的角度來看,LiferayObjectConstructor
可以說是ObjectConstructor
的複製品,在某些場景下可能會起到關鍵作用。
迴歸正題,此時我們只剩下一條思路——JSONFactoryImpl
物件。不難發現,這個物件擁有著一系列與JSON有關的方法,其中包括serialize
和deserialize
方法。
重點關注其deserialize
方法,因為我們可以控制傳入的JSON字串,從而反序列化出我們需要的物件。此處_jsonSerializer
為LiferayJSONSerializer
物件(繼承自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
[2] FreeMarker Java Template Engine
https://freemarker.apache.org/
[3] FreeMarker 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模板注入實現遠端命令執行