JavaSec系列 - 4. 反序列化与JNDI注入(2)
本章源码: https://github.com/hey3e/JavaSec-Code/tree/main/javasec4
深入分析CVE-2021-21344。
上章,我们以fastjson的JSON.parseObject
为反序列化入口,JdbcRowSetImpl
类的lookup
方法为JNDI注入点,实现了一次完整的攻击。
类似,XML解析库XStream使用toXML
和fromXML
方法对Java对象进行序列化与反序列化,我们来看下它如何成为攻击的触发点。
首先了解一下基本原理。用下列代码处理上章的Person
类:
1 | package javasec4; |
序列化后的XML为:
1 | <javasec4.Person> |
接下来我们对Person
类稍作处理,使其实现Serializable
接口并重写readObject
方法:
1 | package javasec4; |
此时序列化后的XML为:
可以注意到两点,对于实现了Serializable
接口并重写了readObject
方法的类,其序列化后的XML会新增serialization="custom"
字段,同时,类似于fastjson反序列化过程中类的getter
和setter
方法会得到执行,XML反序列化过程中,类重写的readObject
会被执行。
记住这些,我们分层来分析CVE-2021-21344的POC:
最外层,是java.util.PriorityQueu
类,一个借助其comparator
进行排序的优先级队列。可知它实现了Serializable
接口并重写了readObject
方法。<default>
标签下,是该类的两个属性size
和comparator
。最下面的便是队列中的两个字符串元素”javax.xml.ws.binding.attachments.inbound”。
为了更好的理解,我们可以自己定义这样一个队列并对其序列化:
1 | public class test { |
查看结果:
除comparator
外,完全一致。
接下来重点看下comparator
部分:
层层嵌套,我们用debug的方式来理解。
在PriorityQueue
的readObject
方法上下断点,发现它被如期调用:
对PriorityQueue
而言,一个序列化后的队列字符串,反序列化要做的就是重新排序,对应readObject
的heapify
方法:
排序的标准便是前文提及的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:
其中,对packet
的message
,此处为com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart
,一个MIME类型的XML,进行了剖析。接下来,我们把注意力集中到message
上,POC部分如下:
来到XMLMessage$XMLMultiPart
的getMessage
方法:
可见,getMessage
最终需要delegate
,而POC中并未构造,因此会借助dataSource
来进行生成。这里的dataSource
为com.sun.xml.internal.ws.message.JAXBAttachment
,POC中可以看到,它有两个属性,bridge
和jaxbObject
。此处具体的生成逻辑为,利用dataSource
提供的序列化操作,即JAXBAttachment
的bridge
,将dataSource
的主体,即JAXBAttachment
的jaxbObject
,转化为字节流,传入MimeMultipartParser
进行处理,以生成delegate
并返回。
对照POC,我们看到这里的jaxbObject
是上章用到过的JdbcRowSetImpl
类,它的详细构造我们等下来看。这里,我们先保持思路跟进封装了一系列序列化操作的bridge
,此处为com.sun.xml.internal.ws.db.glassfish.BridgeWrapper
的com.sun.xml.internal.bind.v2.runtime.BridgeImpl
类:
来到序列化方法marshal
:
可见,该方法首先利用context
的marshallerPool
构造了一个Marshaller
。于是我们跟进到POC的<context>
:
可以发现<context>
中<nameList>
提供的两个属性均为空,这是因为其内容并不会影响到攻击的主逻辑,但是是必须的,如果没有则会报错。这里我展示了二者分别是在何处被访问到的:
<namespaceURIs>
:
<localNames>
:
分析完context
,我们继续看序列化操作:
其中,t
为JdbcRowSetImpl
类,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中构造的bi
:ClassBeanInfoImpl
,的serializeURIs
方法对JdbcRowSetImpl
进行序列化:
这里我们看到<uriProperties>
是不可或缺的。接着,调用了inheritedAttWildcard
的get
方法,此处为com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection
,顾名思义,用于反射访问类的getter
和setter
。而这里的get
,反射调用了JdbcRowSetImpl
的getter
方法:
上章,攻击借助了JdbcRowSetImpl
的setAutoCommit
方法,而实际上,它的一个getter
方法,getDatabaseMetaData
,也调用了connect
,也就是我们在POC中所声明的。因此至此,我们已经从XStream反序列化的入口,挖掘到了JNDI注入点,整体的调用栈如下:
最后我们回头填前文<jaxbObject>
的坑:
有了上章的经验,dataSource
一目了然,攻击的最后一环也就绪了。
但是我们注意到奇怪的一点,这里dataSource
被声明在了JdbcRowSetImpl
的父类BaseRowSet
中,而JdbcRowSetImpl
本身并没有得到构造。
这与XML序列化的特点有关。我们构造一个Student
类,使其继承自Person
,并为其增加一个grade
属性:
1 | public class Student extends Person implements Serializable { |
查看其序列化后的XML:
可见父类Person
的属性age
被声明在<javasec4.Student>
外。以此类推,由于dataSource
实际上归属于BaseRowSet
,因此只需在其下声明即可。
完整的POC如下:
1 | <java.util.PriorityQueue serialization='custom'> |
共55行,相较于官网的103行,精简了许多。
总结一下,CVE-2021-21344的关键词有两个,PriorityQueue
和JAX-WS (Java API For XML-WebService)。首先,XStream反序列化时,进入了PriorityQueue
的反序列化方法,接着,借助JAX-WS的相关类,对XML再次进行了序列化,从而触发了JNDI注入。
JavaSec系列 - 4. 反序列化与JNDI注入(2)
https://hey3e.github.io/2022/01/17/JavaSec系列-4-反序列化与JNDI注入-2/