Kryo
前言
本来想缓两天再写Hessian的,但是被卷到了emmm。
讨厌卷狗。
Hessian太多了,一次写不完的,那我这次就写写Kryo吧,下次写表达式注入SSTI之类的,Hessian遥遥无期了hhhh。
Kryo 是一个快速序列化/反序列化工具,依赖于字节码生成机制(底层使用了 ASM 库),因此在序列化速度上有一定的优势,但正因如此,其使用也只能限制在基于 JVM 的语言上(Scala、Kotlin)
其他类似的序列化工具:原生JDK、Hessian、FTS
官方文档:https://github.com/EsotericSoftware/kryo
Start
1 | |
待序列化目标类MyClass:
1 | |
1 | |
Ser & Deser
Kryo提供了三组方法来读写对象
- 类未知且对象可能为null
1
2kryo.writeClassAndObject(output, object);
Object object = kryo.readClassAndObject(input);
- 类已知且对象可能为null
1
2kryo.writeObjectOrNull(output, object);
SomeClass object = kryo.readObjectOrNull(input, SomeClass.class);
- 类已知且对象不为null
1
2kryo.writeObject(output, object);
SomeClass object = kryo.readObject(input, SomeClass.class);
这些方法首先都是找到合适的序列化器(serializer),再进行序列化或反序列化,序列化器会递归地调用这些方法。
Kryo的注册
Kryo为了提供性能和减小序列化结果体积,提供注册序列化对象类的方式。
在注册时,会为该序列化类生成int ID, 后续在序列化时使用int ID唯一标识该类型
1 | |
序列化流程
跟进writeClassAndObject:

Registration获取
writeClass(output, object.getClass())返回一个object类的Registration
若该类没有注册过(也就是没有上面的kryo.register指定一个类),会自动使用默认的序列化器注册,注册有两个目的:获取序列化器和类的唯一标识Id,方便后续的序列化和反序列化
1 | |
com.esotericsoftware.kryo.util.ObjectMap类维护了一个Class与Registration(含相对应的反序列化器)的对应表:

当然我们自定义的类肯定在这个表中找不到,里面都是Java的基础类,DefaultClassResolver#getRegistration就返回null。
接着进入registerImplicit -> getDefaultSerializer继续找一些Java内置类是否和待序列化类对应

继续跟进,发现FieldSerializer作为默认序列化器,并在FieldSerializer#rebuildCachedFields中获取序列化类的Fields,忽略静态成员。

到此就获取到了自定义类的Registration。
Field序列化
接着进入FieldSerializer.write(this, output, object);
1 | |
Kryo封装了一个UnsafeUtil(Unsafe对象通过反射获取)
1 | |
在JVM中,对实例的Field进行了有规律的存储,通过一个偏移量可以从内存中找到相应的Field值
unsafe实现了在内存层面,通过成员字段偏移量offset来获取对象的属性值
接着获取成员的序列化器,步骤跟上面的一样(getRegistration(type).getSerializer())

剩下的就是继续递归所有成员,获取序列化器进行序列化。
反序列化流程
同样也是先获取类的Registration,再从Registration拿序列化器。

FieldSerializer#read首先对类进行实例化,这里是使用了Kryo封装的com.esotericsoftware.reflectasm#ConstructorAccess去构造类对象,基于ASM,还没学过ASM,就不深入跟进去看了。

同样是获取成员的序列化器,递归调用readObject:

可以跟一下这里的readObjectOrNull
1 | |
这里的序列化器是StringSerializer,直接从输入流input读取了,否则就继续调用上面的FieldSerializer#read了
后面的setField也是用unsafe从内存层面往成员偏移量处填充值

反序列化结束。
可总结为:

Attack
网上找到的Kryo反序列化问题都是在Dubbo那块的。
Dubbo默认的序列化协议是Hessian,但可以修改Dubbo协议数据包中的header,指定SerializationID,来确定Consumer和Provider通信使用的序列化协议,这里就不细讲Dubbo数据包的修改了,而是抽取其中关键的Kryo反序列化,Dubbo相关的具体可以看大佬写的。
调用栈
getTransletInstance:455, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
write:-1, ASMSerializer_1_TemplatesImpl (com.alibaba.fastjson.serializer)
write:270, MapSerializer (com.alibaba.fastjson.serializer)
write:44, MapSerializer (com.alibaba.fastjson.serializer)
write:280, JSONSerializer (com.alibaba.fastjson.serializer)
toJSONString:863, JSON (com.alibaba.fastjson)
toString:857, JSON (com.alibaba.fastjson)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
put:612, HashMap (java.util)
read:162, MapSerializer (com.esotericsoftware.kryo.serializers)
read:39, MapSerializer (com.esotericsoftware.kryo.serializers)
readClassAndObject:813, Kryo (com.esotericsoftware.kryo)
Kryo从input中读取解析到type为HashMap
因此会调用MapSerializer序列化器来读取input中的信息
既然是Map的反序列化就肯定涉及到键值对的处理
MapSerializer会将解析到的key和value都通过调用map.put()来放入HashMap对象中
接着调用putVal(),equals()判断两个键是否相对
com.sun.org.apache.xpath.internal.objects.XString#equals会调用toString

org.springframework.aop.target.HotSwappableTargetSource#equals
1 | |
多套一个HotSwappableTargetSource是为了让HashMap的putVal能走到equals
这里触发com.alibaba.fastjson.JSON类的toString()函数,进而调用JSONSerializer的write()函数,从而触发Fastjson Gadget。
参考: