Xmldecoder学习之路

0x01 前言

之前看到@fnmsd师傅关于xmldecoder的解析流程分析文章《XMLDecoder解析流程分析》,然后我就想着自己也跟一下,我测试的java版本是1.8.0_171。

0x02 测试代码

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
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.beans.XMLDecoder;

public class Xmldecoder {

public static void XMLDecode_Deserialize(String path) throws Exception {
File file = new File(path);
FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis);
XMLDecoder xd = new XMLDecoder(bis);
xd.readObject();
xd.close();
}


public static void main(String[] args){
//XMLDecode Deserialize Test
String path = "/Users/l1nk3r/Desktop/poc.xml";
try {
XMLDecode_Deserialize(path);
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_131" class="java.beans.XMLDecoder">
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="2">
<void index="0">
<string>open</string>
</void>
<void index="1">
<string>/Applications/Calculator.app</string>
</void>
</array>
<void method="start" />
</object>
</java>

0x03 前置知识

这里我选择在 java.lang.ProcessBuilder 接收 array 类型数据传入的地方下个断点。

可以看到整个解析流程涉及到的类和构造方法大概就是如下所示,这里我们看到一个在 package:com.sun.beans.decoderDocumentHandler 类中 parse 解析了,我们的传入的xml文件。

这里可以跟进一下 DocumentHandler 类,这个类继承了 DefaultHandler

跟进一下 DefaultHandler 类,我们可以知道它是使用 sax 来解析 xml 的默认 handler ,而且代码里来看它主要实现了 EntityResolver , DTDHandler , ContentHandler , ErrorHandler 这四个 handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.xml.sax.helpers;
import java.io.IOException;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.DTDHandler;
import org.xml.sax.ContentHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
* Default base class for SAX2 event handlers.
*
...
public class DefaultHandler
implements EntityResolver, DTDHandler, ContentHandler, ErrorHandler
{

回过头来看 DocumentHandler 这个类,在这个类的构造方法中,我们看到了 XMLDecoder 对每种支持的标签都实现了一个继承与 ElementHandler 的类。

所以从目前来看 DocumentHandler 这个构造函数主要的作用就是创建各个标签对应的 ElementHandler 并进行调用。当然关于 DocumentHandler 类因为是继承了 DefaultHandler 类,这一点我们前面聊到了,而 DefaultHandler 类中解析xml的工作主要交付给了 ContentHandler 接口,所以我们可以借鉴一下这个接口是怎么个解析xml内容的,相关内容可以参考Java Sax的ContentHandler的文档,这里直接借鉴一下@fnmsd师傅写的内容,因为我觉得我自己写的话没有他写的这么的棒。

startElement
处理开始标签,包括属性的添加
DocumentHandler:XML解析处理过程中参数包含命名空间URL、标签名、完整标签名、属性列表。根据完整标签名创建对应的ElementHandler并添加相关属性,继续调用其startElement。

ElementHandler: 除了array标签以外,都无操作。

endElement
结束标签处理函数
DocumentHandler: 调用对应ElementHandler的endElement函数,并将当前ElementHandler回溯到上一级的ElementHandler。

ElementHandler: 没看有重写的,都是调用抽象类ElementHandler的endElement函数,判断是否需要向parent写入参数和是否需要注册标签对象ID。

characters
DocumentHandler: 标签包裹的文本内容处理函数,比如处理java.lang.ProcessBuilder包裹的文本内容就会从这个函数走。函数中最终调用了对应ElementHandler的addCharacter函数。

addCharacter
ElementHandler: ElementHandler里的addCharacter只接受接种空白字符(空格\n\t\r),其余的会抛异常,而StringElementHandler中则进行了重写,会记录完整的字符串值。

addAttribute
ElementHandler: 添加属性,每种标签支持的相应的属性,出现其余属性会报错。

getContextBean
ElementHandler: 获取操作对象,比如method标签在执行方法时,要从获取上级object/void/new标签Handler所创建的对象。该方法一般会触发上一级的getValueObject方法。

getValueObject
ElementHandler: 获取当前标签所产生的对象对应的ValueObject实例。具体实现需要看每个ElementHandler类。

isArgument
ElementHandler: 判断是否为上一级标签Handler的参数。

addArgument
ElementHandler: 为当前级标签Handler添加参数。

XMLDecoder相关的其它
两个成员变量,在类的实例化之前,通过对parent的调用进行增加参数。

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

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

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

而sax解析xml文件中的单个node节点(根节点)的流程在 DefaultHandler 中一般是三步走: startElement->characters->endElement

针对node节点(非根)的解析流程在中是四步走 DefaultHandler 中一般是四步走: startElement->characters->endElement->characters。

所以总结来看本质上最有关系的还是这三个构造方法 startElement、characters、endElement

然后,我们可以看看 DocumentHandler 中的这些 ElementHandler 的继承关系,这里重点表扬一下 ideauml 的功能,使用方法很简单,选择一个你想要看的方法,右键选择 Diagrams

然后每次选择 add Class to Diagram 就可以增加你要看的相关类。

最后就生成了下面这张图。

而关于xmldecoder对应的xml文件语法书写如下所示:

  • 每个元素表示方法调用。
  • “对象”标签表示一个表达式 ,其值将用作封闭元素的参数。
  • “void”标签表示将被执行的语句 ,但其结果将不会用作封闭方法的参数。
  • 包含元素的元素使用这些元素作为参数,除非它们具有标签:“void”。
  • 方法的名称由“method”属性表示。
  • XML的标准“id”和“idref”属性用于对前面的表达式进行引用,以便处理对象图形中的循环。
  • “类”属性用于明确指定静态方法或构造函数的目标; 其值是该类的完全限定名称。
  • 如果没有由“class”属性定义目标,则使用“void”标签的元素将使用外部上下文作为目标来执行。
  • Java的String类被特别处理,并写入 Hello,world </ string>,其中使用UTF-8字符编码将字符串的字符转换为字节。

0x04 流程跟进

这里跟进一下上面的那些基础知识分析过的内容,首先按照我们的了解来看,针对xml文件的解析最早应该是使用 DefultHandlerstartElement ,我们在这里下个断点。

回过头来看一下xml文件内容,最开始的节点叫java,所以这里解析出来对应的对应的 handlerJavaElementHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_131" class="java.beans.XMLDecoder">
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="2">
<void index="0">
<string>open</string>
</void>
<void index="1">
<string>/Applications/Calculator.app</string>
</void>
</array>
<void method="start" />
</object>
</java>

JavaElementHandler 中的 addAttribute 构造函数的作用就是添加相应的属性,比如 version 的属性是版本信息这里是 1.8.0_131 ,而另一个就是 class 这里是 java.beans.XMLDecocer

1
2
3
4
5
6
7
8
public void addAttribute(String var1, String var2) {
if (!var1.equals("version")) {
if (var1.equals("class")) {
this.type = this.getOwner().findClass(var2);
} else {
super.addAttribute(var1, var2);
}
}

所以开始第一次 addAttribute 的结果是 version 和它对应的值,当然这个解析过程不是一次,单单是下面内容就会执行两次解析过程。

1
java version="1.8.0_131" class="java.beans.XMLDecoder"

当然关键不在这,我想知道为什么能够执行 java.lang.ProcessBuilder 这个类,从前面debug过程来看,如果。startElement 解析道这里处理之后对应的 handler 应该是 ObjectElementHandler

1
2
3
4
5
6
7
8
9
10
11
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="2">
<void index="0">
<string>open</string>
</void>
<void index="1">
<string>/Applications/Calculator.app</string>
</void>
</array>
<void method="start" />
</object>

在这里下一个断点,果然进来了, ObjectElementHandler 里面的 addAttribute 方法没有能够解析 class 的选项。

但是由于 ObjectElementHandler 继承了 NewElementHandler ,所以这里便使用 NewElementHandler 中的 addAttribute 方法来处理,这里自然有了能够处理 class 的能力了。

这里的 type 自然就是名字是 java.lang.ProcessBuilder 的类。

这里处理完了之后就会继续调用 ArrayElementHandler 处理 array 的内容了,然后调用 VoidElementHandler 处理 void 标签的内容,而 VoidElementHandler 也是继承来自于 ObjectElementHandler ,自然而然也是回到了 ObjectElementHandler 中的 addAttribute 中了。

这里我就下了两个断点,我们可以看到 void 标签中的 indexmethod 处理过程自然都进入到了这里。

当然前面过程 void 标签处理完之后,首先会进入 DocumentHandlerendElement 方法进行处理,而这个方法中调用了 ElementHandler 中的 endElement 构造方法。

1
2
3
4
5
6
7
8
9
10
//DocumentHandler
public void endElement(String var1, String var2, String var3) {
try {
this.handler.endElement();
} catch (RuntimeException var8) {
this.handleException(var8);
} finally {
this.handler = this.handler.getParent();
}
}

而在 endElement 构造方法中调用了 getValueObject ,这个方法设置了在 endElement 中被设置成了一个抽象类,我们通过在 java.lang.ProcessBuilder 处下一个断点,可以看到自然进到了 NewElementHandler 中的 getValueObject 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void endElement() {
ValueObject var1 = this.getValueObject();
if (!var1.isVoid()) {
if (this.id != null) {
this.owner.setVariable(this.id, var1.getValue());
}

if (this.isArgument()) {
if (this.parent != null) {
this.parent.addArgument(var1.getValue());
} else {
this.owner.addObject(var1.getValue());
}
}
}
}

再回顾一下继承关系,这里我放大了一下这部分内容。

上面的 NewElementHandler 中的构造方法 getValueObjectObjectElementHandlerArrayElementHandlerMethodElementHandler 中分别被重写了。

因为我们 payload 里面关于命令执行使用的是 object 标签,所以自然就进来到了 ObjectElementHandler 中,然后这里有调用了 getContextBean 方法,而根据继承关系,这个方法调用的是 NewElementHandler 中的 getContentBean 方法

1
2
3
protected final Object getContextBean() {
return this.type != null ? this.type : super.getContextBean();
}

NewElementHandler 中的 getContextBean 方法又是因为继承关系调用了来自于 ElementHandler 中的 getContextBean 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected Object getContextBean() {
if (this.parent != null) {
ValueObject var2 = this.parent.getValueObject();
if (!var2.isVoid()) {
return var2.getValue();
} else {
throw new IllegalStateException("The outer element does not return value");
}
} else {
Object var1 = this.owner.getOwner();
if (var1 != null) {
return var1;
} else {
throw new IllegalStateException("The topmost element does not have context");
}
}

而这里的 getValueObjcet 操作根据我的断点执行了两次。

我们可以看一下结果,第一次进入 NewElementHandler 中进行 getValueObject 实际上在 type 里面是没有 class 的值的。而第二次进入 NewElementHandler 中进行 getValueObject 操作的时候 type 是取到相关的 class 的值。

那么根据上面所说这里势必会进入到 ObjectElementHandler 中的 getValueHandler 方法进行处理,而在这个方法的最后是调用了 Expression 中的 getValue 方法,这里可以看到我们要执行的命令以及 class 对象都已经传入了。

而进一步动态调试我们可以看到 Expression 中的 getValue 已经返回我们需要的命令执行对象类,以及命令参数了。

然后会重新回到 Void 标签对应的 getValueObject 函数,由于 ViodElementHandler 继承 ObjectElementHandler ,而我们这里设置的 method 等于 start ,因此这里通过 Expression 的反射机制调用了 start 函数,所以到这里自然命令执行了。

这个是从师傅文章里看到的小贴士,自己跟下来之后确实是这样

PS: 虽然ObjectElementHandler继承自NewElementHandler,但是其重写了getValueObject函数,两者是使用不同方法创建类的实例的。

再PS: 其实不加java标签也能用,但是没法包含多个对象了。

然后再补充一下@fnmsd师傅画的流程图

0x05 补丁

Oracle关于这个xmldecoder造成的漏洞的CVE编号分别是CVE-2017-3506、CVE-2017-10271、CVE-2019-2725。最早关于CVE-2017-3506的补丁只是根据Object标签进行了限制。

而根据我们前面的继承关系可以讲 object 替换成 void 即可,它们实际上是不受影响的,因此便出现了CVE-2017-10271,而针对CVE-2017-10271的补丁限定了所有具有执行的节点。

但这次 CVE-2019-2725 主要是 class 标签,class 标签可代替 object 标签来生成对象,因此这次漏洞本质还是xmldecoder的问题,而补丁也是针对class标签来处理的。