目前。网上关于CVE-2015-4852漏洞的资料很多,但是针对CVE-2015-4852漏洞如何修复,修复补丁又是如何生效的却少之又少;而CVE-2016-0638、CVE-2016-3510这两个漏洞又是如何绕过CVE-2015-4852补丁的,则只是在介绍Weblogic系列漏洞时被一句话带过。

CVE-2015-4852、CVE-2016-0638以及CVE-2016-3510,这三个漏洞有着极其相似的地方,其本质就是利用了Weblogic反序列化机制,而官方在修复CVE-2015-4852时,也并未对这个机制进行调整,而仅仅是在此基础上增加了一个关卡:黑名单。

因此,在彻底搞清楚Weblogic反序列化漏洞的原理以及如何修复这个问题之前,很有必要弄清楚Weblogic处理流量中的java反序列化数据的流程。只有清楚了这一点,才能很好的理解如下几个问题:

  1. CVE-2015-4852是如何产生的以及后续是如何修复的?

  2. 修复CVE-2015-4852,为何要在resolveClass:108,InboundMsgAbbrev$ServerChannelInputStream (weblogic.rjvm)处添加黑名单?

  3. CVE-2016-0638、CVE-2016-3510是如何绕过修复?二者的绕过方式有何相同与不同?

Weblogic 反序列化攻击时序

为了搞清楚CVE-2015-4852、CVE-2016-0638、CVE-2016-3510中的种种疑团,我们需要首先来弄明白一些原理性的东西,我们先从Weblogic
反序列化攻击时序入手,看看Weblogic是如何从流量中将序列化字节码进行反序列化。

首先贴出一张Weblogic 反序列化攻击时序图

f165b1f00063791e06a1dbd615604c52.png

这张图是从我的好朋友廖新喜大佬博客扒下来的,也欢迎大家去读一读他的关于java漏洞的分析文章:

http://xxlegend.com/2018/06/20/%E5%85%88%E7%9F%A5%E8%AE%AE%E9%A2%98%20Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AE%9E%E6%88%98%20%E8%A7%A3%E8%AF%BB/

上图为一张完整的Weblogic反序列化攻击时序图,庞大而且繁杂,不如我们将其拆分开,首先说说Weblogic如何从流量数据取出序列化数据并获取其类对象的过程。

从流量数据到Class对象

首先我们来看一张图:

ccd71bc1337e47616a30ed30beaa656c.png

Weblogic通过7001端口,获取到流量中T3协议的java反序列化数据。从上图中readObject开始,经过流程中的一步步的加工,并最终于上图流程终点处的resolveProxyClass或resolveClass处将流量中的代理类/类类型的字节流转变为了对应的Class对象。

首先我们可以发现:在ObjectInputStream (java.io)中的readClassDesc方法处,存在着分叉点,导致了序列化流量流向了两个不同的分支:其中一些流量流向了readProxyDesc并最终采用resolveProxyClass获取类对象,而另一些则流向了readNonProxyDesc并最终使用resolveClass获取类对象。

readClassDesc是什么?

从上文来看,流量数据经过readClassDesc并驶入了不同的处理分支。

首先来看一下readClassDesc方法的官方注释:“readClassDesc方法读入并返回(可能为null)类描述符。将passHandle设置为类描述符的已分配句柄。”

如果想理解官方注释的含义,需要扩充一些java序列化的知识:

java序列化数据在流量传输,并不是随随便便杂乱无章的,序列化数据的格式是要遵循序列化流协议。

序列化流协议定义了字节流中传输的对象的基本结构。该协议定义了对象的每个属性:其类,其字段以及写入的数据,以及以后由类特定的方法读取的数据。

字节流中对象的表示可以用一定的语法格式来描述。对于空对象,新对象,类,数组,字符串和对流中已有对象的反向引用,都有特殊的表示形式。比如说在字节流中传递的序列化数据中,字符串有字符串类型的特定格式、对象有对象类型的特定格式、类结构有着类结构。而TC_STRING、TC_OBJECT、TC_CLASSDESC则是他们的描述符,他们标识了接下来这段字节流中的数据是什么类型格式的

以TC_CLASSDESC为例,TC_CLASSDESC在流量中的值是(byte)0x72,在序列化流协议中,当这个值出现后,代表接下来的数据将开始一段Class的描述(DESC=description),即TC_CLASSDESC描述符(byte)0x72后面的字节流数据为Class类型。通过这些描述符,程序可以正确的解析流量中的序列化数据。

如果对这部分感兴趣,可以参照oracle文档:

https://www.oracle.com/security-alerts/cpuoct2020traditional.html

readClassDesc的功能很简单:读入字节流,通过读取字节流中的描述符来确定字节流中传递数据的类型,并交给对应的方法进行处理。

接下来我们看看readClassDesc的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   private ObjectStreamClass readClassDesc(boolean unshared) 
throws IOException
{
byte tc = bin.peekByte();
switch (tc) {
case TC_NULL:
return (ObjectStreamClass) readNull();

case TC_REFERENCE:
return (ObjectStreamClass) readHandle(unshared);

case TC_PROXYCLASSDESC:
return readProxyDesc(unshared);

case TC_CLASSDESC:
return readNonProxyDesc(unshared);

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
}

从readClassDesc方法的实现可见,readClassDesc中switch语句有5个分支(TC_NULL、TC_REFERENCE、TC_PROXYCLASSDESC、TC_CLASSDESC、default)。

1
2
3
4
5
6
7
TC_NULL描述符表示空对象引用

TC_REFERENCE描述符表示引用已写入流的对象

TC_PROXYCLASSDESC是新的代理类描述符

TC_CLASSDESC是新的类描述符

那么我们为什么在上文流程图里只画出了其中两处分支(TC_PROXYCLASSDESC、TC_CLASSDESC)呢?

我们先来看看Weblogic反序列化漏洞成的原理:Weblogic反序列化漏洞是由于通过流量中传入的恶意类而未得到合理的过滤,最终被反序列化而形成。

从原理上来看,是weblogic对流量中序列化后的类对象处理时出现的问题。

基于这一点,我们应重点关注程序是如何从流量中获取并处理类类型数据的流程。

TC_PROXYCLASSDESC与TC_CLASSDESC描述符标识了流量中代理类与类这两种类型的数据,因此我们重点关注TC_PROXYCLASSDESC与TC_CLASSDESC这两处分支,这也是上文流程图里只有这两处分支的原因。

当readClassDesc从字节流中读取到TC_CLASSDESC描述符,说明此处程序此时要处理的字节流为普通类,程序接下来会调用readNonProxyDesc方法对这段字节流进行解析。

在readNonProxyDesc方法中,程序会从该段序列化流中获取类的序列化描述符ObjectStreamClass(类序列化描述符ObjectStreamClass,其本质是对Class类的包装,可以想象成一个字典,里面记录了类序列化时的一些信息,包括字段的描述信息和serialVersionUID 和需要序列化的字段fields,以便在反序列化时拿出来使用)。随后该类的序列化描述符被传递给resolveClass方法,resolveClass方法从该类的序列化描述符中获取对应的Class对象。

6f8788495652b7967dbc4b94b7452790.png

当readClassDesc从字节流中读取到TC_PROXYCLASSDESC描述符时,说明此处程序此时要处理的字节流为动态代理类,程序接下来会调用readProxyDesc方法进行处理,过程与上文一致,不再复述。

我们以此处传入的字节流为普通类为例,接下来看看resolveClass是如何将类的序列化描述符加工成该类的Class对象

位于weblogic/rjvm/InboundMsgAbbrev.class中的resolveClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
protected Class resolveClass(ObjectStreamClass var1) throws ClassNotFoundException, IOException {
Class var2 = super.resolveClass(var1);
if (var2 == null) {
throw new ClassNotFoundException("super.resolveClass returns null.");
} else {
ObjectStreamClass var3 = ObjectStreamClass.lookup(var2);
if (var3 != null && var3.getSerialVersionUID() != var1.getSerialVersionUID()) {
throw new ClassNotFoundException("different serialVersionUID. local: " + var3.getSerialVersionUID() + " remote: " + var1.getSerialVersionUID());
} else {
return var2;
}
}
}

程序通过Class var2 = super.resolveClass(var1); 从ObjectStreamClass var1中获取到对应的类对象,并赋值给var2,最终通过执行return var2,将var1序列化描述符所对应的Class对象返回

我们以熟悉的CVE-2015-4852利用链为例,动态调试一下resolveClass方法

b35be97a6451bd74a8868173bdd629d0.png

可见resolveClass方法成功从序列化描述符中获取到”sun.reflect.annotation.AnnotationInvocationHandler”类对象,并将其返回

到目前为止,我们已经搞明白了weblogic如何将流量中的类字节流转变为对应的Class对象。以上这部分知识,有助于我们理解Weblogic官方的修复方案。而接下来我们要谈论的是在Weblogic获得到Class对象后要做的事情,通过对这部分流程的理解,将会帮助你很轻松的理解为什么CVE-2015-4852、CVE-2016-0638、CVE-2016-3510的poc是如何奏效的。

从Class对象到代码执行

通过上文的介绍可知,程序通过resolveClass获取Class对象,在resolveClass方法将获取到的Class对象返回后,上一级的readNonProxyDesc在接收到resolveClass方法返回值后,连同之前从流量中获取类的序列化描述符ObjectStreamClass一并,初始化并构建一个新的ObjectStreamClass,这个流程如下:

image-20201029110755721

关键部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private ObjectStreamClass readNonProxyDesc(boolean unshared) 
throws IOException
{
...

ObjectStreamClass desc = new ObjectStreamClass();
...

ObjectStreamClass readDesc = null;
...
readDesc = readClassDescriptor();
...

Class cl = null;
...
if ((cl = resolveClass(readDesc)) == null) {
resolveEx = new ClassNotFoundException("null class");
}
...
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));

...
return desc;
}

结合流程图与代码来看,readNonProxyDesc方法中主要做了如下这些事情

1、通过readClassDescriptor()方法从流量中获取序列化类的ObjectStreamClass并赋值给readDesc变量

2、将readDesc传入resolveClass,获取该类的Class对象并赋值给cl变量

3、将该类的ObjectStreamClass与Class对象传入initNonProxy方法,初始化一个ObjectStreamClass并赋值给desc变量

4、将desc变量返回

readNonProxyDesc方法中返回的ObjectStreamClass类型的desc变量,将会传递给readClassDesc,进而被readClassDesc传递给readOrdinaryObject

readOrdinaryObject、readClassDesc与readNonProxyDesc的调用关系如下:

readOrdinaryObject中调用readClassDesc:

1
2
3
4
5
6
7
private Object readOrdinaryObject(boolean unshared) 
throws IOException
{
...

ObjectStreamClass desc = readClassDesc(false);
...

readClassDesc中调用readNonProxyDesc:

1
2
3
4
5
6
7
8
private ObjectStreamClass readClassDesc(boolean unshared) 
throws IOException
{
byte tc = bin.peekByte();
switch (tc) {
...
case TC_CLASSDESC:
return readNonProxyDesc(unshared);

因此,当readNonProxyDesc中ObjectStreamClass类型的desc变量返回后,途径readClassDesc方法的最终传递给readOrdinaryObject中的desc变量

接下来看看readOrdinaryObject方法中部分片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ObjectStreamClass desc = readClassDesc(false);
...
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}

handles.finish(passHandle);

if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);

上述代码中的第一行:

1
ObjectStreamClass desc = readClassDesc(false);

代码中由readClassDesc(false)执行得到的desc,即是readNonProxyDesc中获取并返回的ObjectStreamClass类型的desc

1
obj = desc.isInstantiable() ? desc.newInstance() : null;

代码中接下来的条件分支,即是在获取了ObjectStreamClass类型的desc后,readOrdinaryObject接着尝试调用类对象中的readObject、readResolve、readExternal等方法。

关于readObject、readResolve、readExternal等方法的调用,将其整理成流程图,有助于对其更好的理解,流程图如下:

54e51374e255a9c4b137acc1d0cbd56c.png

在Weblogic从流量中的序列化类字节段通过readClassDesc-readNonProxyDesc-resolveClass获取到普通类序列化数据的类对象后,程序依次尝试调用类对象中的readObject、readResolve、readExternal等方法。

在这里提前透露下,CVE-2015-4852、CVE-2016-0638、CVE-2016-3510这三个漏洞,所利用的恰好依次是恶意类”sun.reflect.annotation.AnnotationInvocationHandler”中的readObject、”weblogic.jms.common.StreamMessageImpl”中的readExternal、以及”weblogic.corba.utils.MarshalledObject”中的readResolve方法

试想一下这个场景:在没有任何防护或防护不当的时候,攻击者通过流量中传入恶意类的序列化数据,weblogic将流量中的序列化数据还原为其对应的Class对象,并尝试执行恶意类中的readObject、readResolve、readExternal等方法。这就是CVE-2015-4852、CVE-2016-0638、CVE-2016-3510漏洞的核心。

CVE-2015-4852

在分析过流程之后,这个漏洞呼之欲出。简单来说,在CVE-2015-4852漏洞爆发之前,weblogic对流量中的序列化数据没有任何的校验,长驱直入的恶意数据最终被还原出其Class对象,并被Weblogic调用了其Class对象中的readObject方法,结合CVE-2015-4852细节来说就是:

  1. 精心构造的ChainedTransformer恶意链(以下简称恶意数据)

  2. 将构造好的恶意数据包裹在AnnotationInvocationHandler类的memberValues变量中

  3. 在流量中构造并传入上述制作好的AnnotationInvocationHandler类的序列化数据

  4. Weblogic获取AnnotationInvocationHandler类的Class对象

  5. Weblogic尝试调用AnnotationInvocationHandler类的readObject方法

  6. AnnotationInvocationHandler类中readObject中存在一些有助于漏洞利用的操作(AnnotationInvocationHandler的readObject方法中对其memberValues的每一项调用了setValue方法进而调用了checkSetValue)

  7. 恶意数据存放在memberValues中伺机而动。恶意数据的原理我们就不细说了,简单来说就是,当恶意数据的checkSetValue被触发,就能造成命令执行

  8. 等到readObject方法对memberValues的每一项调用setValue方法执行时,setValue方法会进而调用并触发恶意数据的checkSetValue,造成命令执行

e92fbde69bafb5ac777da44e96bd9d9a.png

Weblogic的防护机制

CVE-2015-4852这个漏洞利用出现之后,官方对Weblogic进行了一些改造,增加了一些安全防护。至于怎么防护,说起来很简单,以普通类为例,见下图:

10b6817babd9d985caefe76cc0623ac0.png

resolveClass方法的作用是从类序列化描述符获取类的Class对象,在resolveClass中增加一个检查,检查一下该类的序列化描述符中记录的类名是否在黑名单上,如果在黑名单上,直接抛出错误,不允许获取恶意的类的Class对象。这样以来,恶意类连生成Class对象的机会都没有,更何况要执行恶意类中的
readObject、readResolve、readExternal呢。

我们看一下具体是怎么实现的,见下图:

b95c0a9e24b511213e076e61a069e12f.png

可见更新之后多出了一个if条件分支,通过isBlackListed校验传入的类名用来处理代理类的resolveProxyClass也是一样的方式,不再复述。

从整体上来看,检查模块主要在下图红框里

898634e47e18bace15c0cbba196c9b31.png

在修复过后,CVE-2015-4852已经不能成功利用了:CVE-2015-4852所使用到的AnnotationInvocationHandler在黑名单中,会直接报错而不能获取其Class对象,更不能执行其中的readObject。

CVE-2016-0638

这个漏洞主要是找到了个黑名单之外的类”weblogic.jms.common.StreamMessageImpl”

简单来说,由于黑名单的限制,CVE-2015-4852利用链没法直接使用,这个漏洞像是整了个套娃,给CVE-2015-4852装进去了。

为什么使用StreamMessageImpl这个类呢?其实原理也很简单。StreamMessageImpl类中的readExternal方法可以接收序列化数据作为参数,而当StreamMessageImpl类的readExternal执行时,会反序列化传入的参数并执行该参数反序列化后对应类的readObject方法。我们动态调试一下,见下图

362088fdc6055340600570c83c08993f.png

如果我们把序列化后的CVE-2015-4852利用链序列化之后丢进readExternal呢?

当我们给上图StreamMessageImpl类的readExternal中传入序列化后的CVE-2015-4852利用链,在readExternal被执行时,会将CVE-2015-4852利用链数据反序列化,并在上图864行处调用其readObject方法,也就是AnnotationInvocationHandler的readObject方法

好了,AnnotationInvocationHandler的readObject方法被调用了,CVE-2015-4852复活了。

但是StreamMessageImpl类的readExternal要怎么被执行呢?别忘了上文分析的Weblogic反序列化流程,在获取到StreamMessageImpl类的Class对象后,程序可不止调用其readObject方法,还会尝试调用readExternal的。

9ce6bbce5fae485feaa76654ff515c80.png

CVE-2016-3510

这个漏洞与上一个几乎一致,也是做了个套娃给CVE-2015-4852利用链装进去了,从而绕过了黑名单限制。

这次找到的是weblogic.corba.utils.MarshalledObject

首先看一下这个类的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public MarshalledObject(Object var1) throws IOException {
if (var1 == null) {
this.hash = 13;
} else {
ByteArrayOutputStream var2 = new ByteArrayOutputStream();
MarshalledObject.MarshalledObjectOutputStream var3 = new MarshalledObject.MarshalledObjectOutputStream(var2);
var3.writeObject(var1);
var3.flush();
this.objBytes = var2.toByteArray();
int var4 = 0;

for(int var5 = 0; var5 < this.objBytes.length; ++var5) {
var4 = 31 * var4 + this.objBytes[var5];
}

this.hash = var4;
}
}

可以发现,这个类的构造方法接收一个Object类型的参数var1,然后将传入的Object参数序列化后转换为byte数组的形式赋值给this.objBytes

我们接下来看看MarshalledObject的readResolve方法

1
2
3
4
5
6
7
8
9
10
11
public Object readResolve() throws IOException, ClassNotFoundException, ObjectStreamException {
if (this.objBytes == null) {
return null;
} else {
ByteArrayInputStream var1 = new ByteArrayInputStream(this.objBytes);
ObjectInputStream var2 = new ObjectInputStream(var1);
Object var3 = var2.readObject();
var2.close();
return var3;
}
}

可见,MarshalledObject的readResolve方法将this.objBytes反序列化,并执行其readObject。this.objBytes可以由MarshalledObject构造方法中传入的var参数控制

d47f1dff5b0fa0d58f49133ceba48d38.png

这样以来,将CVE-2015-4852利用链传入MarshalledObject构造方法中,将MarshalledObject序列化后,则可以将CVE-2015-4852利用链保存在其this.objBytes变量中。当weblogic将构造好的MarshalledObject反序列化时,weblogic将尝试调用MarshalledObject的readResolve方法时,CVE-2015-4852利用链被执行

如果对如何构造的poc细节不是很清楚,可以参照这个链接

https://github.com/zhzhdoai/Weblogic_Vuln/blob/master/Weblogic_Vuln/src/main/java/com/weblogcVul/CVE_2016_3510.java

总结

Weblogic的前三个反序列化漏洞并不复杂,但是对其的研究和分析是有一定的方式的,仅仅从单一的漏洞分析入手,很容易看不懂,反而把漏洞弄得复杂化。在了解了漏洞流程之后,再回头来看,这几个漏洞的原理便呼之欲出。