一 、原理
(一)概述
接CVE-2015-4852,
Oracle使用黑名单进行了防护,如下,
看一下CVE-2015-4852调试过程中记录的信息,
functors在3个点完全被禁,
再看看触发时的调用栈,
结合上一次调试过程中记录的信息,我们可以看到,这个fix阻断了CVE-2015-4852的exp中在偏底层的位置(ServerChannelInputStream)对关键类的反序列化,可以说是打断了利用链。
但同时,我们也要注意到,对那几个关键类的反序列化的禁止仅存在于那三个列出的反序列化点。也就是说,如果能在别的反序列化点,通过适当的readObject对那几个关键类进行反序列化,也有可能能触发RCE之类的漏洞。
实际上,CVE-2016-3510、CVE-2016-0638和后来的几个漏洞的成功攻击就是基于对这三个反序列化点的黑名单的绕过。
(二)CVE-2016-0638
由于CVE-2015-4852的修复是基于黑名单的,所以我们如果能找到一个对象,其可以在自己的readObject中创建自己的InputStream的对象,并且不是在黑名单中的三个反序列化点进行的反序列化,最后调用readObject()方法进行反序列化的数据的读取,这样仍然可以执行含有恶意代码的序列化代码,甚至CVE-2015-4852的攻击链都不需要做太多的修改。
就有大牛发现weblogic.jms.common.StreamMessageImpl的readExternal(),
此函数可以进行反序列化操作的,而且不在这个黑名单之列,也就是说可以绕过这个基于黑名单的补丁。
(三)原理
这个漏洞的复现主要基于一个类似于ysoserial的工具——weblogic_cmd。
接下来结合着自己的理解和调试过程谈一下漏洞的原理。
使用Github上的工具weblogic_cmd,
这个工具直接当做IDEA的工程打开即可,要求不高的话甚至不需要引入别的库。
首先,我们看到Main.java中起关键作用的函数executeBlind(),
配置application参数,
配置好参数后,debug即可,
跟进blindExecute(),里面会根据os参数的情况选择是“cmd”或是“/bin/bash”,继续跟进serialBlindData,
下面我们会看到,这里面进行的就是常规的序列化操作,
这里我们看到,其实这里就是封装ysoserial的commons collections1的链,
可以看到,这里进行的操作和CVE-2015-4852中构造的攻击链并无二致,
这几个函数进行的是反序列化的操作,我们步出即可,
可见这里的payload的类型为StreamMessage类,
然后写出,
继续向下执行,就到发送payload的一步了,
T3ProtocolOperation.send()会将payload发出,另一个调试服务器的IDEA工程就会弹出,这一部分和漏洞的调试部分是一起完成的,关于漏洞的调试参见第二章的第三部分。
结合下面的调试进行分析。
二、调试
(一)环境搭建
这次完全使用docker里的源码及jar文件进行调试,借鉴了一个良心博客,虽然最后搭建的环节应该没太大区别而且整个过程技术含量不高,但接触一点和之前不一样的东西总没坏处。
语法,
docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH
这里使用,
docker cp 7dee:/root /tmp
然后idea打开/root/Oracle/Middleware/wlserver_10.3/目录
然后使用命令把Middleware目录下所有的*.jar包都放在一个test的文件夹里,命令如下:
find ./ -name *.jar -exec cp {} ./test/ \;
但这种方法有个坑,就是同名的jar只能存在一个,对于这个漏洞的调试似乎没有影响,对于后面的漏洞(如CVE-2020-14645)的调试可能有一定影响(别问我怎么知道的,如果有同志发现并无影响也希望能联系我)。
然后在libraries下添加test目录,
在jdk这块选用weblogic10.3.6自带的jdk6,
然后我们像之前一样添加远程服务器即可,
(二)复现
使用Github上的工具,
配置remote,Program arguments如下,
-H "192.168.0.47" -C "touch /tmp/sss1.txt" -B -os linux
run一下,
查看目录,
复现成功。
(三)调试
接着工具的分析,发送完payload之后docker那边就能收到,进而反馈到IDEA中,
可以看到,进入了StreamMessageImple的readExternal函数,其中var4为一个getInputStream函数的返回值,这一块就是本漏洞调试的关键点,
以view as text查看var4,可以看到这就是反序列化数据,接下来var5以var4为参数创建了一个ObjectInputStream,并调用了自身的readObject(重点,这也就是为什么大牛选择了这种类而不是其他不具备这个特点的类),到这里和CVE-2015-4852中攻击链中不一样的地方基本就讲完了,个人感觉就是反序列化的触发点换了一下。
讲个故事,一种药(payload)吃到孩子的胃(weblogic)里可以治病(get shell),小孩子吃了一次(CVE-2015-4852),嫌药苦,便不愿直接吃下,但病总得治(总要被攻击的),于是大人把药包到胶囊(StreamMessageImpl)里面,待到胶囊进入胃里,外壳便会化掉(readExternal()),药物(原有的payload)便会流出,进而发挥作用(get shell)。
跟进var5.readObject(),
进入了一堆底层函数,如何就进入了AnnotationInvocationHandler的invoker,
接下来的流程就比较熟悉了,
由于是ysoserial的cc链1,这里走invoke,
调用LazyMap.get(“entrySet”);下面的流程就和CVE-2015-4852的复现别无二致。
查看docker情况,
由时间差来看,sss1正是刚刚payload创建的
三、收获与启示
CVE-2016-0638主要还是对CVE-2015-4852的黑名单的绕过,内容上差别不大,所谓差异仅仅是触发点换了一下(由ServerChannelInputStream换到了自己的ReadExternal中的InputStream),因而绕过了黑名单。个人感觉是新瓶旧酒,攻击链甚至都不需要有太大的改动。
结合调用栈我们可以看到,
下面的InboundMsgAbbrev什么的都还在,只是参数不再是普通的AnnotationInvocationHandler,而是StreamMessageImpl,而这个类里面的readExternal也能反序列化其中的成员变量,这和下面的mashalledObject有异曲同工之妙。
Oracle没有完全禁掉那几个关键类的反序列化而只是添加了个Filter必有高论,但那几个禁止了的反序列化点对于大牛来说实在是起不到非常有效的防护作用,找到了weblogic.jms.common.StreamMessageImpl的readExternal()也能发挥作用。这也就警示我们,基于黑名单的防护是存在不小的风险的,尤其是像weblogic这样代码量比较大的程序,说不定在哪里还会有别的能起到类似作用的触发点,令安全人员防不胜防。
但是对于这样庞大的一个程序,再加上其引入的众多外部库,我一时也不知道如何用白名单去做防护,以我的水平能想到的只有应该进行对系统函数调用的限制。毕竟防护必须面面俱到,攻击攻破一点即可。