1. 程式人生 > >從WebLogic看反序列化漏洞的利用與防禦

從WebLogic看反序列化漏洞的利用與防禦

0x00 前言

上週出的 WebLogic 反序列漏洞,跟進分析的時候發現涉及到不少 Java 反序列化的知識,然後借這個機會把一些 Java 反序列化漏洞的利用與防禦需要的知識點重新捋一遍,做了一些測試和除錯後寫成這份報告。文中若有錯漏之處,歡迎指出。

0x01 Java 反序列化時序

Java 反序列化時序對於理解 Java 反序列化的利用或是防禦都是必要的,例如有些 Gadget 為什麼從 readObject 方法開始進行構造,為什麼反序列化防禦程式碼寫在 resolveClass 方法中等。先寫下三個相關的方法。

1.1 readObject

這個方法用於讀取物件,這裡要說的 readObject 跟很多同名的這個方法完全不是一回事的,注意下圖中的方法描述符跟其它同名方法的區別。

java.io.ObjectInputStream 類的註釋中有提到,要是想在序列化或者反序列化的過程中做些別的操作可以通過在類中實現這三個方法來實現。比如類 EvilObj 實現了這裡的 readObject 方法(方法的描述符需要跟註釋提到的一樣)的話,在類 EvilObj 的反序列化過程就會呼叫到這個 readObject 方法,程式碼例子:

其呼叫棧如下

看下 readSerialData 方法,在讀取序列化資料的時候做判斷若是該類實現了 readObject 方法,則通過反射對該方法進行呼叫。

到這裡就能明白為什麼有些 Java 反序列化利用的構造是從這裡 readObject 方法開始的,然後通過 readObject 中的程式碼一步一步去構造最終達成利用,這次的 CVE-2018-3191 就是很好的一個例子,後文會講到 CVE-2018-3191 使用的 Gadget。當然這只是 Java 反序列化利用構造的其中一種方法,更多的可以參考 ysoserial 裡的各種 Gadget 的構造。

1.2 resolveClass 和 resolveProxyClass

這兩個方法都是在類 java.io.ObjectInputStream 中,resolveClass 用於根據類描述符返回相應的類,resolveProxyClass 用於返回實現了代理類描述符中所有介面的代理類。這兩個類的功能使得它們可以被用於 Java 反序列的防禦,比如在 resolveClass 方法中可以先對類名進行檢測然後決定是否還要繼續進行反序列化操作。如果想要在這兩個方法中新增一些操作(比如前面提到的做反序列化防禦),那處理資料流的類需要繼承 java.io.ObjectInputStream ,然後重寫下面對應的方法:

protected Class<?> resolveClass(ObjectStreamClass desc)
protected Class<?> resolveProxyClass(String[] interfaces)

這裡需要避免混淆的一點是這兩個方法是在處理資料流的類中重寫,而不是在被反序列化的類中重寫,程式碼例子:

其呼叫棧如下

同理 resolveProxyClass 的重寫方式也是這樣。這裡要知道的一點是並非在 Java 的反序列化中都需要呼叫到這兩個方法,看下呼叫棧前面的 readObject0 方法中的部分程式碼:

看 switch 程式碼塊,假如序列化的是一個 String 物件,往裡跟進去是用不到 resolveClass 或 resolveProxyClass 方法的。resolveProxyClass 方法也只是在反序列化代理物件時才會被呼叫。通過檢視序列化資料結構非常有助於理解反序列化的整個流程,推薦一個用於檢視序列化資料結構的工具:SerializationDumper

1.3 反序列化時序

貼一張廖新喜師傅在“JSON反序列化之殤”議題中的反序列化利用時序圖,用於從整體上看反序列化的流程。

普通物件和代理物件的反序列化走的流程是不一樣的,可以看 readClassDesc 方法:

對應著前面時序圖中例項化的那一步的不同流程。

1.4 小結

這一章主要是介紹了 Java 反序列化相關的三個方法,通過程式碼跟蹤除錯的方式來確定其在什麼時候會被呼叫到,再結合反序列化的時序圖就可以對反序列化的整個流程有一定的瞭解。其實去分析了下反序列化的時序主要是為了知道兩點,第一個是反序列化的大體流程,第二個是有哪些方法在這流程中有被呼叫到,為了解 Java 反序列化的利用和防禦做一些知識準備。

0x02 WebLogi T3 反序列化及其防禦機制

T3 從 WebLogic 的啟動到對訊息進行序列化的呼叫棧(由下往上):

at weblogic.rjvm.InboundMsgAbbrev.readObject(InboundMsgAbbrev.java:73)
at weblogic.rjvm.InboundMsgAbbrev.read(InboundMsgAbbrev.java:45)
at weblogic.rjvm.MsgAbbrevJVMConnection.readMsgAbbrevs(MsgAbbrevJVMConnection.java:283)
at weblogic.rjvm.MsgAbbrevInputStream.init(MsgAbbrevInputStream.java:214)
at weblogic.rjvm.MsgAbbrevJVMConnection.dispatch(MsgAbbrevJVMConnection.java:498)
at weblogic.rjvm.t3.MuxableSocketT3.dispatch(MuxableSocketT3.java:348)
at weblogic.socket.BaseAbstractMuxableSocket.dispatch(BaseAbstractMuxableSocket.java:394)
at weblogic.socket.SocketMuxer.readReadySocketOnce(SocketMuxer.java:960)
at weblogic.socket.SocketMuxer.readReadySocket(SocketMuxer.java:897)
at weblogic.socket.PosixSocketMuxer.processSockets(PosixSocketMuxer.java:130)
at weblogic.socket.SocketReaderRequest.run(SocketReaderRequest.java:29)
at weblogic.socket.SocketReaderRequest.execute(SocketReaderRequest.java:42)
at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:145)
at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:117)

這裡沒有去分析 T3 協議的具體實現,抓了一下 stopWebLogic.sh 在執行過程中的資料包:

第一個是握手包,然後第二個包中就可以找到帶有序列化資料了,包的前 4 個位元組為包的長度。替換序列化資料那部分,然後做資料包重放就可以使得 T3 協議反序列化的資料為自己所構造的了。

2.1 WebLogic 的反序列化防禦機制

從呼叫棧可以知道是在哪裡做的反序列化,InboundMsgAbbrev 類的 readObject 方法:

留意下這裡的 readObject 方法的描述符,跟前一章提的 readObject 方法描述符是不一樣的,也就是說假如反序列化一個 InboundMsgAbbrev 物件,這裡的 readObject 方法是不會被呼叫到的。這裡的 readObject 只是在 T3 協議處理訊息的程式碼流程中被使用到。

可以看到處理輸入資料流的類為 ServerChannelInputStream,由前一小節知道輸入流是可以被控制的,接下來就是例項化 ServerChannelInputStream 物件然後進行反序列化操作。先看下 ServerChannelInputStream 類:

ServerChannelInputStream 類的繼承圖:

可以得知 ServerChannelInputStream 類是繼承了 ObjectInputstream 類的,並且重寫了 resolveClass 和 resolveProxyClass 方法。由上一章的內容可以知道 ServerChannelInputStream 類中的這兩個方法在對不同的序列化資料進行反序列化的時候會被呼叫到,這樣就不難理解 WebLogic 為什麼會選擇在這兩個方法中新增做過濾的程式碼了(其實之前出現的針對反序列化的防禦方法也有這麼做的,重寫 ObjectInputstream 類中的 resolveClass 方法或者直接重寫一個 ObjectInputstream)。

先說一下 resolveProxyClass 這個方法裡為什麼用代理的介面名字和 “java.rmi.registry.Registry” 進行對比,這個是 CVE-2017-3248 漏洞的補丁。CVE-2017-3248 漏洞的利用用到 JRMPClient 這個 Gadget,ysoserial 中的 JRMPClient 用到了動態代理,代理的介面就是 “java.rmi.registry.Registry”。針對這個就出現了不少的繞過方法,比如換一個介面 java.rmi.activation.Activator,或者直接不使用代理都是可以的。這裡涉及到了 JRMPClient 這個 Gadget 的具體構造,但這不屬於本文的內容,想了解這個的話建議去看 ysoserial 中具體是如何構造實現的。

resolveClass 方法中的 checkLegacyBlacklistIfNeeded 方法是用來針對類名和包名做過濾。

從 checkLegacyBlacklistIfNeeded 方法跟進去直到進入 WebLogicObjectInputFilter 類的 checkLegacyBlacklistIfNeeded 方法:

可以看到這裡是在 Jre 自帶的過濾(JEP290)不可用的情況下才會使用自身實現的方法進行過濾,如果檢測到是在黑名單中會丟擲異常 Unauthorized deserialization attempt。看下 isBlacklistedLegacy 方法:

可以看到要是類名第一個字元為 [(在欄位描述符中是陣列)或是 primitiveTypes(一些基礎資料型別)中的其中一個,是不會進行檢測的。

檢測的地方有兩個,一個是類名,一個包名,只要其中一個出現在 LEGACY_BLACKLIST 中便會像前面看到的丟擲異常。下面來看一下 LEGACY_BLACKLIST 的值是從哪裡來的。

看 WebLogicObjectInputFilter 的一個初始化方法:

在 Jre 的過濾不可用的情況下會設定 LEGACY_BLACKLIST 的值,跟入 getLegacyBlacklist 方法:

值來自於 WebLogicFilterConfig 類的成員變數 BLACKLIST,BLACKLIST 的值由 constructLegacyBlacklist 方法生成:

這裡的引數var1,var2 和 var3 對應著

也就是說還可以通過啟動引數來控制是否新增黑名單,動態新增或刪除一些黑名單。預設情況下的話黑名單就是來自 WebLogicFilterConfig 類中的 DEFAULT_BLACKLIST_PACKAGES 和 DEFAULT_BLACKLIST_CLASSES 了。

打了十月份補丁之後的黑名單如下:

private static final String[] DEFAULT_BLACKLIST_PACKAGES = new String[]{"org.apache.commons.collections.functors", "com.sun.org.apache.xalan.internal.xsltc.trax", "javassist", "java.rmi.activation", "sun.rmi.server"};
private static final String[] DEFAULT_BLACKLIST_CLASSES = new String[]{"org.codehaus.groovy.runtime.ConvertedClosure", "org.codehaus.groovy.runtime.ConversionHandler", "org.codehaus.groovy.runtime.MethodClosure", "org.springframework.transaction.support.AbstractPlatformTransactionManager", "java.rmi.server.UnicastRemoteObject", "java.rmi.server.RemoteObjectInvocationHandler", "com.bea.core.repackaged.springframework.transaction.support.AbstractPlatformTransactionManager", "java.rmi.server.RemoteObject"}

2.2 WebLogic 使用 JEP290 做的過濾

JEP290 是 Java9 新新增可以對序列化資料進行檢測的一個特性。之後往下對 8u121,7u131 和 6u141 這幾個版本也支援了。該特性可用於對序列化資料的最大位元組數,深度,陣列大小和引用數進行限制,當然還有對類的檢測了。使用這個的方法可以為實現 ObjectInputFilter 介面(低版本的 JDK 只在 sun.misc 包中有這個類,Java9 以上在 java.io 包中,目前 Oracle 對 Java9 和 Java10 都停止支援了,最新為 Java11),然後重寫 checkInput 方法來對序列化資料進行檢測。高版本的 JDK 中 RMI 就有用到這個來做過濾,看下 WebLogic 是如何使用的,JDK 版本為 8u152。

WebLogic 是通過反射來獲取到 java.io.ObjectInputFilter 或是 sun.misc.ObjectInputFilter 的各個方法的方式來實現一個 JreFilterApiProxy 物件:

determineJreFilterSupportLevel 方法:

後面的流程大抵如下,根據 DEFAULT_BLACKLIST_PACKAGES 和 DEFAULT_BLACKLIST_CLASSES 的值來給 WebLogicFilterConfig 物件中的成員變數 serialFilter 賦值,serialFilter 的值是作為 JEP290 對序列化資料進行檢測的一個格式(裡面包含需要做檢測的預設值,用分號隔開。包名後面需要帶星號,包名或者類名前面帶感嘆號的話表示黑名單,沒有則表示白名單。這些在 ObjectInputFilter 這個介面的方法中都能看到)。接下來就是反射呼叫 setObjectInputFilter 方法將 serialFilter 的值賦給 ObjectInputStream 中的 serialFilter(假如 ObjectInputStream 物件中的 serialFilter 值為空是不會對序列化資料進行檢測的)。看一下 WebLogic 設定好的 serialFilter:

再看 ObjectInputStream 這邊,圖的左下可以看到從反序列化到進入檢測的呼叫棧:

跟入 checkInput 方法:

前面有些常規的檢測,紅圈部分是針對 serialFilter 裡的格式進行檢測的,這裡用到了 Function<T, U> 介面和 lambda 語法。看下 ObjectInputFilter 介面中的內部類 Global 的程式碼塊就能明白這裡是咋做的檢測了。

看到它是做的字串對比(類名和包名)。再回到 ObjectInputStream 類中的 filterCheck 方法程式碼塊的下面:

只要返回的狀態是空或者 REJECTED 就直接會丟擲異常結束反序列流程了。其它的返回狀態只做一個日誌記錄。

2.3 小結

這一章中可以看到 WebLogic 針對反序列化的防禦方法有兩種,分別對應著 JEP290 不可用和可用的這兩種情況。JEP290 這個的程式碼邏輯還是挺長的,所以在寫分析的時候並沒有把每一步的具體內容都寫上。這兩種方法都是用黑名單的方式來做的過濾,其實它們也不是不能做成白名單,個人覺得白名單的方式應該很容易影響程式的功能,因為 Java 中各種介面和類的封裝導致搞不清在反序列化的時候會用到哪些介面或類,所以寫程式碼的時候不好去確定這樣一個白名單出來。目前來看這樣的過濾方式只有說是在找到新的 Gadget 的情況下才能繞過,從另一個角度來看這樣的過濾也使得這裡會一直存在問題,只是問題還沒被發現。

0x03 WebLogic 遠端除錯及10月補丁修復的漏洞

3.1 WebLogic 遠端除錯

修改 domain/bin/setDomainEnv.sh,設定 debugFlag 為true

這樣啟動的時候會監聽 8453 作為除錯埠,然後使用 Idea 之類的 IDE 建立一個遠端除錯的配置連線到該埠就可以。需要把 WebLogic 中 jar 包新增到專案中去。因為 WebLogic 沒有原始碼,除錯時的程式碼都是反編譯得到的,所以有監控不到變數或者執行的位置跟程式碼行對不上的問題。

3.2 CVE-2018-3245

這個洞是 7 月份 CVE-2018-2893 的補丁還沒有修復完善導致的繞過,涉及到 JRMPClient 這個 Gadget 的構造,具體可以參考Weblogic JRMP反序列化漏洞回顧

這裡提一點,黑名單中新增的類名不是直接序列化物件的類名而是它的父類類名能做到過濾效果的原因是在序列化資料中是會帶上父類類名的。

3.3 CVE-2018-3191

這個 Gadget 不是新的,只是在 com.bea.core.repackaged.springframework 這個包裡還有相關的類。

結合第一章提到的 readObject 這個 Gadget 是非常好理解的,只是還需要知道 JNDI 的利用方式才能完整實現利用。

com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager 這個類在進行反序列化的時候會觸發 JNDI 查詢,結合針對 JNDI 的利用便可以做到程式碼執行的效果。

JtaTransactionManager 類的 readObject 方法:

進入 initUserTransactionAndTransactionManager 方法:

進入 lookupUserTransaction 方法再往下跟很快就可以看到 JDNI 的查詢方法 lookup:

針對 JNDI 的一個利用前提便是 lookup 方法的引數可控,即 name 的值能被傳入成一個 RMI 或者 LDAP 的絕對路徑。從前面的程式碼可以知道這裡的 name 的值來自於 JtaTransactionManager 類中的成員變數 transactionManagerName,因此只要設定 transactionManagerName 值為可控的 RMI 地址,然後將 JtaTransactionManager 物件序列化後通過 T3 協議傳輸給 WebLogic 便可以在 T3 協議對資料進行反序列化的時候完成利用。

利用演示:

因為是針對 JDNI 的利用,所以要想在預設的情況下進行利用需要 JDK 的版本小於 8u121 或者 7u131(因為高於這些版本預設情況下已經將 trustURLCodebase 的值設為 false,使得不能做遠端類載入),同時伺服器需要能夠連線外網。

0x04 參考連結