目前。网上关于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反序列化数据的流程。只有清楚了这一点,才能很好的理解如下几个问题:
CVE-2015-4852是如何产生的以及后续是如何修复的?
修复CVE-2015-4852,为何要在resolveClass:108,InboundMsgAbbrev$ServerChannelInputStream (weblogic.rjvm)处添加黑名单?
CVE-2016-0638、CVE-2016-3510是如何绕过修复?二者的绕过方式有何相同与不同?
Weblogic 反序列化攻击时序
为了搞清楚CVE-2015-4852、CVE-2016-0638、CVE-2016-3510中的种种疑团,我们需要首先来弄明白一些原理性的东西,我们先从Weblogic
反序列化攻击时序入手,看看Weblogic是如何从流量中将序列化字节码进行反序列化。
首先贴出一张Weblogic 反序列化攻击时序图
这张图是从我的好朋友廖新喜大佬博客扒下来的,也欢迎大家去读一读他的关于java漏洞的分析文章:
上图为一张完整的Weblogic反序列化攻击时序图,庞大而且繁杂,不如我们将其拆分开,首先说说Weblogic如何从流量数据取出序列化数据并获取其类对象的过程。
从流量数据到Class对象
首先我们来看一张图:
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 | private ObjectStreamClass readClassDesc(boolean unshared) |
从readClassDesc方法的实现可见,readClassDesc中switch语句有5个分支(TC_NULL、TC_REFERENCE、TC_PROXYCLASSDESC、TC_CLASSDESC、default)。
1 | TC_NULL描述符表示空对象引用 |
那么我们为什么在上文流程图里只画出了其中两处分支(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对象。
当readClassDesc从字节流中读取到TC_PROXYCLASSDESC描述符时,说明此处程序此时要处理的字节流为动态代理类,程序接下来会调用readProxyDesc方法进行处理,过程与上文一致,不再复述。
我们以此处传入的字节流为普通类为例,接下来看看resolveClass是如何将类的序列化描述符加工成该类的Class对象
位于weblogic/rjvm/InboundMsgAbbrev.class中的resolveClass方法
1 | protected Class resolveClass(ObjectStreamClass var1) throws ClassNotFoundException, IOException { |
程序通过Class var2 = super.resolveClass(var1); 从ObjectStreamClass var1中获取到对应的类对象,并赋值给var2,最终通过执行return var2,将var1序列化描述符所对应的Class对象返回
我们以熟悉的CVE-2015-4852利用链为例,动态调试一下resolveClass方法
可见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,这个流程如下:
关键部分代码如下
1 | private ObjectStreamClass readNonProxyDesc(boolean unshared) |
结合流程图与代码来看,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 | private Object readOrdinaryObject(boolean unshared) |
readClassDesc中调用readNonProxyDesc:
1 | private ObjectStreamClass readClassDesc(boolean unshared) |
因此,当readNonProxyDesc中ObjectStreamClass类型的desc变量返回后,途径readClassDesc方法的最终传递给readOrdinaryObject中的desc变量
接下来看看readOrdinaryObject方法中部分片段
1 | ObjectStreamClass desc = readClassDesc(false); |
上述代码中的第一行:
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等方法的调用,将其整理成流程图,有助于对其更好的理解,流程图如下:
在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细节来说就是:
精心构造的ChainedTransformer恶意链(以下简称恶意数据)
将构造好的恶意数据包裹在AnnotationInvocationHandler类的memberValues变量中
在流量中构造并传入上述制作好的AnnotationInvocationHandler类的序列化数据
Weblogic获取AnnotationInvocationHandler类的Class对象
Weblogic尝试调用AnnotationInvocationHandler类的readObject方法
AnnotationInvocationHandler类中readObject中存在一些有助于漏洞利用的操作(AnnotationInvocationHandler的readObject方法中对其memberValues的每一项调用了setValue方法进而调用了checkSetValue)
恶意数据存放在memberValues中伺机而动。恶意数据的原理我们就不细说了,简单来说就是,当恶意数据的checkSetValue被触发,就能造成命令执行
等到readObject方法对memberValues的每一项调用setValue方法执行时,setValue方法会进而调用并触发恶意数据的checkSetValue,造成命令执行
Weblogic的防护机制
CVE-2015-4852这个漏洞利用出现之后,官方对Weblogic进行了一些改造,增加了一些安全防护。至于怎么防护,说起来很简单,以普通类为例,见下图:
resolveClass方法的作用是从类序列化描述符获取类的Class对象,在resolveClass中增加一个检查,检查一下该类的序列化描述符中记录的类名是否在黑名单上,如果在黑名单上,直接抛出错误,不允许获取恶意的类的Class对象。这样以来,恶意类连生成Class对象的机会都没有,更何况要执行恶意类中的
readObject、readResolve、readExternal呢。
我们看一下具体是怎么实现的,见下图:
可见更新之后多出了一个if条件分支,通过isBlackListed校验传入的类名用来处理代理类的resolveProxyClass也是一样的方式,不再复述。
从整体上来看,检查模块主要在下图红框里
在修复过后,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方法。我们动态调试一下,见下图
如果我们把序列化后的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的。
CVE-2016-3510
这个漏洞与上一个几乎一致,也是做了个套娃给CVE-2015-4852利用链装进去了,从而绕过了黑名单限制。
这次找到的是weblogic.corba.utils.MarshalledObject
首先看一下这个类的构造方法
1 | public MarshalledObject(Object var1) throws IOException { |
可以发现,这个类的构造方法接收一个Object类型的参数var1,然后将传入的Object参数序列化后转换为byte数组的形式赋值给this.objBytes
我们接下来看看MarshalledObject的readResolve方法
1 | public Object readResolve() throws IOException, ClassNotFoundException, ObjectStreamException { |
可见,MarshalledObject的readResolve方法将this.objBytes反序列化,并执行其readObject。this.objBytes可以由MarshalledObject构造方法中传入的var参数控制
这样以来,将CVE-2015-4852利用链传入MarshalledObject构造方法中,将MarshalledObject序列化后,则可以将CVE-2015-4852利用链保存在其this.objBytes变量中。当weblogic将构造好的MarshalledObject反序列化时,weblogic将尝试调用MarshalledObject的readResolve方法时,CVE-2015-4852利用链被执行
如果对如何构造的poc细节不是很清楚,可以参照这个链接
总结
Weblogic的前三个反序列化漏洞并不复杂,但是对其的研究和分析是有一定的方式的,仅仅从单一的漏洞分析入手,很容易看不懂,反而把漏洞弄得复杂化。在了解了漏洞流程之后,再回头来看,这几个漏洞的原理便呼之欲出。