作者:DEADF1SH_CAT @ 知道創宇404實驗室
時間:2020年8月24日

前言

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接口。但該列表會受到沙箱的限制,其中有一部分對象被封禁,無法被調用。

這些便是通過模板 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>.

既然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


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1304/