从CVE-2019-2729谈Weblogic XML RCE的绕过史

*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担

从 CVE-2017-3506 为起点至今,weblogic 接二连三的吧爆出了大量的反序列化漏洞,而这些反序列化漏洞的很大一部分,都是围绕着 XMLDecoder 的补丁与补丁的绕过展开的,所以笔者以 CVE-2017-3506 为起点,到近期的 CVE-2019-2725 及其绕过来谈一谈这两年 weblogic 在 XMLDecoder 上的缝缝补补。

认识 XMLDecoder

首先去看一下 XMLDecoder 的官方文档,如下:

XMLDecoder 类用于读取使用 XMLEncoder 创建的 XML 文档,用途类似于 ObjectInputStream。例如,用户可以使用以下代码片段来读取以 XML 文档形式(通过 XMLEncoder 类写入)定义的第一个对象:

XMLDecoder d = new XMLDecoder(new BufferedInputStream(new FileInputStream("Test.xml")));    Object result = d.readObject();    d.close();

作为一名 java 反序列化的研究人员,看到 readObject() 函数就应该带有一丝兴奋,至少代表我们找到入口了。

先不去管在 weblogic 上的利用,我们先构造一个特殊的 poc.xml 文件,让 XMLDecoder 去解析一下,看一下流程

                                                        /bin/bash                                                    -c                                                    ls                                                    

再写一个简单的利用 XMLDecoder 解析 xml 文件的 demo,

import java.beans.XMLDecoder;    import java.io.*;    public class Main {        public static void main(String[] args) throws IOException, InterruptedException {            File file = new File("poc.xml");            XMLDecoder xd = null;            try {                xd = new XMLDecoder(new BufferedInputStream(new FileInputStream(file)));            } catch (Exception e) {                e.printStackTrace();            }            Object s2 = xd.readObject();            xd.close();        }    }

因为会触发命令执行,所以先直接在 ProcessBuilder 的 start 函数上打上断点,看一下调用栈,

我们关注的重点在于从 xml 到 ProcessBuilder 类被实例化的过程,所以去跟进一下 DocumentHandler 类,我们去看几个核心函数,

首先看到了构造函数,看一看到为不同的标签定义了不同的 Handler,

再看一下 startElement 函数,它用来实例化对应的 Element,并给当前 handler 设置 Owner 和 Parent,关于 Owner 和 Parent,直接引用 @fnmsd 写的内容:

parent

最外层标签的 ElementHandler 的 parent 为 null,而后依次为上一级标签对应的 ElementHandler

owner

ElementHandler: 固定 owner 为所属 DocumentHandler 对象。

DocumentHandler: owner 固定为所属 XMLDecoder 对象。

然后看一下 endElement 函数,

他会直接调用对应的 ElementHandler 的 endElement 函数,代码如下,

接下来一连串的 Handler 的 getValueObject 调用之后,到达了 ObjectElementHandler 的 getValueObject 函数,并在该函数内将我们标签内的值传给了 Expression 类,

在调用了 getValue 方法后,成功将 ProcessBuilder 类的实例返回,

接下来再返回给 VoidElementHandler 将 start 函数传过来,调用 start 函数,命令执行成功。

最后补上一张 @ fnmsd 给出的 XMLDecoder 解析 xml 的流程图以加深理解。

CVE-2017-3506

上一节已经可以看到,XMLDecoder 在解析 xml 的时候,通过构造特殊的 xml 文件是可以造成命令执行的,接下来我们就可以来看一下第一个 weblogic 由于 XMLDEcoder 导致的命令执行漏洞 CVE-2017-3506。

先上 POC,

                                                                                                                                            /bin/bash                                                                                        -c                                                                                         open /Applications/Calculator.app/                                                                                                                                                

调用链我们只跟到 XMLDecoder.readObject(),因为剩下的都是上一节的内容了,

在 processRequest 函数中,会对传入的 payload 进行分割,把真正的 xml 交给 readHeaderOld 函数处理,

readHeaderOld 函数则是将真正的 xml 传给 XMLDecoder,并在后续的一连串调用中将 XMLDecoder 实例化调用其 readObject 函数,于是便造成了命令执行。

CVE-2017-10271

在 CVE-2017-3506 爆出后,我们去看一下官方的补丁,代码如下:

private void validate(InputStream is) {          WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();          try {             SAXParser parser = factory.newSAXParser();             parser.parse(is, new DefaultHandler() {                public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {                   if(qName.equalsIgnoreCase("object")) {                      throw new IllegalStateException("Invalid context type: object");                   }                }             });          } catch (ParserConfigurationException var5) {             throw new IllegalStateException("Parser Exception", var5);          } catch (SAXException var6) {             throw new IllegalStateException("Parser Exception", var6);          } catch (IOException var7) {             throw new IllegalStateException("Parser Exception", var7);          }       }

补丁非常的简单,一旦标签是 object,系统报错,于是立马出了第二版的 poc,CVE-2017-10271:

                                                                                                                                            /bin/bash                                                                                        -c                                                                                         open /Applications/Calculator.app/                                                                                                                                                

乍一看这个 poc,简直和 CVE-2017-3506 一模一样,唯一得到区别就是

变成了

仅仅是类的标签类型由 object 变成了 void,我们去看一下 VoidElementHandler 的源码:

可以看到 VoidElementHandler 是 ObjectElementHandler 类的子类,这也就解释了为什么把 object 标签换成 Void 标签也同样可以造成命令执行。

CVE-2019-2725

时隔一年多,CVE-2019-2725 爆出,这次的漏洞是要分两块来看的,

1、 新爆出的存在反序列化的组件_async
2、 CVE-2017-10271 的补丁被绕过

首先看第一点,在 ProcessBuilder 的 start 函数上打一个断点,先看一下 async 组件在处理 xml 时候的调用链(老规矩只追到 XMLDecoder.readObject 函数),

引用廖大神的分析思路,请求会经过 webservice 注册的 21 个 Handler 来处理,看一下 HandlerIterator 类,就能发现对应的 21 个 Handler,

21 个 Handler 里面 AsyncResponseHandler 应该是我们重点关注的那一个,跟进去看一下源码的 handleRequest 方法,

可以看到要想让程序往下走,必须保证 var2 有值,也就是 RelatesTo 有值,这也就是为什么 payload 里面有

xx    xx

这两行的原因。

关于 WS-Addressing 的使用,也可以去参考官方文档( https://www.w3.org/Submission/ws-addressing/ ),有助于深刻理解。

走过各种 Handler 后来到 WorkAreaServerHandler,对 xml 进行了拆分,接下来的调用就和前面一样了(xml 交给 XMLDecoder,调用 readObject 方法),

分析完 async 组件后,就来到了另一个问题上,如何绕过 CVE-2017-10271 的补丁,老套路,我们先看一下补丁内容,

private void validate(InputStream is) {          WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();          try {             SAXParser parser = factory.newSAXParser();             parser.parse(is, new DefaultHandler() {                private int overallarraylength = 0;                public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {                   if(qName.equalsIgnoreCase("object")) {                      throw new IllegalStateException("Invalid element qName:object");                   } else if(qName.equalsIgnoreCase("new")) {                      throw new IllegalStateException("Invalid element qName:new");                   } else if(qName.equalsIgnoreCase("method")) {                      throw new IllegalStateException("Invalid element qName:method");                   } else {                      if(qName.equalsIgnoreCase("void")) {                         for(int attClass = 0; attClass < attributes.getLength(); ++attClass) {                            if(!"index".equalsIgnoreCase(attributes.getQName(attClass))) {                               throw new IllegalStateException("Invalid attribute for element void:" + attributes.getQName(attClass));                            }                         }                      }                      if(qName.equalsIgnoreCase("array")) {                         String var9 = attributes.getValue("class");                         if(var9 != null && !var9.equalsIgnoreCase("byte")) {                            throw new IllegalStateException("The value of class attribute is not valid for array element.");                         }

这次的补丁内容我们文字化一下:

1、 禁用 object、new、method 标签
2、 如果使用 void 标签,只能有 index 属性
3、 如果使用 array 标签,且标签使用的是 class 属性,则它的值只能是 byte

这次的补丁可以说是比上一次严格的多,前两点虽然很大程度上限制了我们不能随意生成对象,调用方法,但好在还有一个 class 标签可以使用,最关键的还在于第三点,它限制了我们的参数不能再是 String 类型,而只能是 byte 类型,所以我们的思路只能从这一点出发,整理一下思路,我们要寻找的是这样一个类:

1、 他的成员变量是 byte 类型
2、 在该类进行实例化的时候就能造成命令执行。

于是便有了 oracle.toplink.internal.sessions.UnitOfWorkChangeSet 来满足我们的需求。

看一下构造函数,该类会对传给它的 byte 值进行反序列化,可以看到这是一个标准的二次反序列化,于是满足二次反序列的 payload 应该都可以用,如 AbstractPlatformTransactionManager、7u21 等等。

具体 payload 如下:

                                        oracle.toplink.internal.sessions.UnitOfWorkChangeSet                                        -84                    ...                    ...                                                                

关于二次反序列的原理不再一一分析,大佬们早已经给出了非常详尽的解释,有兴趣可以去廖大神的博客( http://xxlegend.com )学习一下,也可以选择读一下 ysoserial 的 7u21 模块代码就 ok。

针对此次漏洞,官方给出的修复补丁处理比较简单,禁用 class 标签。

private void validate(InputStream is) {       WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();       try {          SAXParser parser = factory.newSAXParser();          parser.parse(is, new DefaultHandler() {             private int overallarraylength = 0;             public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {                if (qName.equalsIgnoreCase("object")) {                   throw new IllegalStateException("Invalid element qName:object");                } else if (qName.equalsIgnoreCase("class")) {                   throw new IllegalStateException("Invalid element qName:class");                } else if (qName.equalsIgnoreCase("new")) {                   throw new IllegalStateException("Invalid element qName:new");                } else if (qName.equalsIgnoreCase("method")) {                   throw new IllegalStateException("Invalid element qName:method");                } else {                   if (qName.equalsIgnoreCase("void")) {                      for(int i = 0; i = WorkContextXmlInputAdapter.MAXARRAYLENGTH) {                               throw new IllegalStateException("Exceed array length limitation");                            }                            this.overallarraylength += length;                            if (this.overallarraylength >= WorkContextXmlInputAdapter.OVERALLMAXARRAYLENGTH) {                               throw new IllegalStateException("Exceed over all array limitation.");                            }

不过 7u21 模块有一点要提一下,7u21 模块利用的最后会通过将 TemplatesImpl 对象的_bytecodes 变量动态生成为对象,于是该类的 static block 和构造函数便会自动执行,而这个类又是攻击者可以随便构造的,于是便造成了命令执行。

由于此次漏洞的 payload 是 byte 写的,而由于攻击利用类又是动态生成的,所以分析攻击者的代码是个比较麻烦的事情,所以下面给出如何将 payload 中攻击者代码还原出来的方法。

1、开启 weblogic 远程调试,并把断点打在 ProcessBuilder 类的 start 函数中(因为此时攻击类已经动态生成成功,但是没法直接在编译器里查看代码)

2、 使用 jps -l 命令查看 weblogic 的 pid

3、运行 sudo java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB 命令查看对应 PID 的内存,

4、搜索内存中的动态生成类,并生成 class 文件,反编译一下,就可以看到攻击者写的自定义类了。

CVE-2019-2725 绕过

最近网上又流传了 CVE-2019-2725 绕过的 poc,如下:

                                                                                    oracle.toplink.internal.sessions.UnitOfWorkChangeSet                                                                                    ...                                                                                                                        

刚拿到 poc 的时候,看了一下思路,因为标签被禁了,所以通过

来绕过补丁。思路是比较清晰的,通过 Class.forName(classname) 来取到我们想要的类,从而绕过 class 标签被禁的问题。

但刚看到这个 poc 的时候,我第一个疑问就是,array 居然可以使用 method 属性吗?所以立马去看了一下 ArrayElementHandler 类的内容,

只支持 length 标签,但是它是 NewElementHandler 的子类,那再去看看 NewElementHandler

支持 class 标签,但是它是 ElementHandler 的子类,再去看一下 ElementHandler

发现到最后也没找到它支持 method 属性。

马上去我自己的环境里面试一下,没法复现成功,一度以为这个 poc 是假的,但后来想了一下,我的环境里面只有 1.7 和 1.8 的 jdk,会不会是 jdk 版本太高了,立马去 1.6 试一下,果然复现成功,看来 1.6 的 XMLDecoder 的代码和 1.7\1.8 不太一样。

去跟进一下 jdk 1.6 的 XMLDecoder,根据原理去写一个简单一点的 poc.xml,测试 demo 继续使用第一章的就行

                        java.lang.ProcessBuilder                                                            /bin/bash                                                    -c                                                    open /Applications/Calculator.app/                                                                

发现 jdk1.6 的 XMLDecoder 代码简单很多,根本没有那么多的 ElementHandler,直接统一放在 ObjectHandler 的代码里面处理。

而对标签的处理,也可以说是非常的朴实无华了,看一下 startElement,

public void startElement(String var1, AttributeList var2) throws SAXException {        ...    ...            String var8 = (String)var3.get("method");            if (var8 == null && var6 == null) {                var8 = "new";            }            var4.setMethodName(var8);         ...    ...            } else if (var1 == "array") {                var14 = (String)var3.get("class");                Class var10 = var14 == null ? Object.class : this.classForName2(var14);                var11 = (String)var3.get("length");                if (var11 != null) {                    var4.setTarget(Array.class);                    var4.addArg(var10);                    var4.addArg(new Integer(var11));                }

我这里只截取关键部分代码,首先可以看到代码根本不管你的标签是什么,只要有 methond 属性,那就算作你的方法名,并且如果你的标签是 array 标签,而有没有 class 属性,自动给你补一个 Class,完美契合需求,所以就可以直接通过 Class.forName 来取到我们需要的类了。

这样也就绕过了对 class 标签的过滤,不过只能在 1.6 的 jdk 利用。

思考与总结

根据近些年 weblogic 由于 XMLDecoder 导致的反序列漏洞的缝缝补补中,可以看到虽然绕过的 poc 层出不穷,但是利用的范围却越来越窄,从一开始的所有 jdk 通用,到 7u21 以下可以利用成功,再到最近的绕过已经只能在 1.6 利用成功,可以看到,保持 jdk 版本的高版本可以有效的防范 java 反序列化攻击。与此同时,对于基本用不到的 weblogic 组件,还是能删就删为好。

引用

http://www.lmxspace.com/2019/06/05/Xmldecoder%E5%AD%A6%E4%B9%A0%E4%B9%8B%E8%B7%AF/

https://blog.csdn.net/fnmsd/article/details/89889144

http://xxlegend.com/2017/12/23/Weblogic%20XMLDecoder%20RCE%E5%88%86%E6%9E%90/

http://xxlegend.com/2019/04/30/CVE-2019-2725%E5%88%86%E6%9E%90/

*本文作者:平安银行应用安全团队-Glassy,转载请注明来自FreeBuf.COM