一次渗透引出的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);
}
}

然后写个发序列化看看,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();
}
}

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

注:引用drun1baby师傅的图片
这几个部分我就不都一一分析了,主要还是分析一下最重要的DynamicProxyConverter 动态代理转换器,这是存在xstream反序列化的漏洞最主要的原因
DynamicProxyConverter 动态代理转换器
DynamicProxyConverter是xstream支持的一种动态代理转换器

他可以将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);
}
}
然后运行之后成功弹出计算器

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

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

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

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


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

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

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

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


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

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


在这个方法里,xstream就开始处理了xml里面其他的节点元素,首先判断是不是第一个元素,是的话就调用putCurrentEntryIntoMap()函数,就是将当前的内容缓存到map中
跟进去看一下,发现调用 readItem() 方法读取标签内的内容并缓存到当前 Map 中

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

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

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

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

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

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

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

后续就是继续的xstream处理xml,都是类似的运行流程
然后最后的最后又会到treeMapConverter.populateTreeMap() 这个地方

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

最后最后调用到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);

浙公网安备 33010602011771号