Kryo

0x01 What Is Kryo

Kryo 是一个快速序列化/反序列化工具,依赖于字节码生成机制(底层使用了 ASM 库),因此在序列化速度上有一定的优势,但正因如此,其使用也只能限制在基于 JVM 的语言上(Scala、Kotlin)

其他类似的序列化工具:原生JDK、Hessian、FTS

官方文档👉https://github.com/EsotericSoftware/kryo

0x02 Quick Start

引入依赖

<dependency>
  <groupId>com.esotericsoftware</groupId>
  <artifactId>kryo</artifactId>
  <version>4.0.2</version>
</dependency>

待序列化目标类MyClass:

public class MyClass {
    public String hello;
    private int num;

   // 省略getter、setter、toString
}
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Test {
    public static void main(String[] args) throws Exception {
        Kryo kryo = new Kryo();
        kryo.register(MyClass.class);
        MyClass myClass = new MyClass();
        myClass.setHello("Hello Kryo");
        myClass.setNum(11);

        Output output = new Output(Files.newOutputStream(Paths.get("file.bin")));
        kryo.writeObject(output, myClass);
        output.close();

        Input input = new Input(Files.newInputStream(Paths.get("file.bin")));
        MyClass obj = kryo.readObject(input, MyClass.class);
        input.close();
        System.out.println(obj);
    }
}
// MyClass{hello='Hello Kryo', num=11}

0x03 Ser and Deser

Kryo提供了三组方法来读写对象

  • 类未知且对象可能为null

kryo.writeClassAndObject(output, object);
Object object = kryo.readClassAndObject(input);
  • 类已知且对象可能为null

kryo.writeObjectOrNull(output, object);
SomeClass object = kryo.readObjectOrNull(input, SomeClass.class);
  • 类已知且对象不为null

kryo.writeObject(output, object);
SomeClass object = kryo.readObject(input, SomeClass.class);

这些方法首先都是找到合适的序列化器(serializer),再进行序列化或反序列化,序列化器会递归地调用这些方法。

Kryo的注册

Kryo为了提供性能和减小序列化结果体积,提供注册序列化对象类的方式。

在注册时,会为该序列化类生成int ID, 后续在序列化时使用int ID唯一标识该类型

kryo.register(SomeClass.class);

序列化流程

跟进writeClassAndObject

Registration获取

writeClass(output, object.getClass())返回一个object类的Registration

若该类没有注册过(也就是没有上面的kryo.register指定一个类),会自动使用默认的序列化器注册,注册有两个目的:获取序列化器和类的唯一标识Id,方便后续的序列化和反序列化

get:351, ObjectMap (com.esotericsoftware.kryo.util)
getRegistration:79, DefaultClassResolver (com.esotericsoftware.kryo.util)
getRegistration:488, Kryo (com.esotericsoftware.kryo)
writeClass:97, DefaultClassResolver (com.esotericsoftware.kryo.util)
writeClass:540, Kryo (com.esotericsoftware.kryo)
writeClassAndObject:645, Kryo (com.esotericsoftware.kryo)
main:16, Test

com.esotericsoftware.kryo.util.ObjectMap类维护了一个ClassRegistration(含相对应的反序列化器)的对应表

当然我们自定义的类肯定在这个表中找不到,里面都是Java的基础类,DefaultClassResolver#getRegistration就返回null

接着进入registerImplicit -> getDefaultSerializer继续找一些Java内置类是否和待序列化类对应

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

到此就获取到了自定义类的Registration

Field序列化

接着进入FieldSerializer.write(this, output, object);

CachedField[] fields = this.fields;
for (int i = 0, n = fields.length; i < n; i++)
    fields[i].write(output, object);

Kryo封装了一个UnsafeUtilUnsafe对象通过反射获取)

java.lang.reflect.Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
_unsafe = (sun.misc.Unsafe)field.get(null);

在JVM中,对实例的Field进行了有规律的存储,通过一个偏移量可以从内存中找到相应的Field值

unsafe实现了在内存层面,通过成员字段偏移量offset来获取对象的属性值

接着获取成员的序列化器,步骤跟上面的一样(getRegistration(type).getSerializer()

剩下的就是继续递归所有成员,获取序列化器进行序列化

反序列化流程

同样也是先获取类的Registration,再从Registration拿序列化器器

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

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

可以跟一下这里的readObjectOrNull

object = (T)serializer.read(this, input, type);

这里的序列化器是StringSerializer,直接从输入流input读取了,否则就继续调用上面的FieldSerializer#read

后面的setField也是用unsafe从内存层面往成员偏移量处填充值

到此反序列化就结束了

总结

0x04 Way To 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

public boolean equals(Object other) {
    return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);
}

多套一个HotSwappableTargetSource是为了让HashMap的putVal能走到equals

这里触发com.alibaba.fastjson.JSON类的toString()函数,进而调用JSONSerializerwrite()函数,从而触发Fastjson Gadget

0x05 Reference

Last updated