# Kryo

## 0x01 What Is Kryo

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

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

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

## 0x02 Quick Start

引入依赖

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

待序列化目标类MyClass：

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

   // 省略getter、setter、toString
}
```

```java
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
>
> ```java
> kryo.writeObjectOrNull(output, object);
> SomeClass object = kryo.readObjectOrNull(input, SomeClass.class);
> ```
>
> * 类已知且对象不为null
>
> ```java
> kryo.writeObject(output, object);
> SomeClass object = kryo.readObject(input, SomeClass.class);
> ```

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

### Kryo的注册

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

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

```java
kryo.register(SomeClass.class);
```

### 序列化流程

跟进`writeClassAndObject`

![image-20230622104241959](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-c010b84d9a2a68a0fbddd9afbfee2f28f68a6146%2Fimage-20230622104241959.png?alt=media)

#### 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`类维护了一个`Class`与`Registration`（含相对应的反序列化器）的对应表

![image-20230622105213753](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-aa29b4479a60aa09fd89e77f9a38e7f7d5e51545%2Fimage-20230622105213753.png?alt=media)

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

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

![image-20230622111002620](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-39c478a6689d81a27e525a8ca99674e1f33db2c5%2Fimage-20230622111002620.png?alt=media)

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

![image-20230622111712485](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-339189c1822e049cf103d8b08d53ea7f0659a106%2Fimage-20230622111712485.png?alt=media)

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

#### Field序列化

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

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

`Kryo`封装了一个`UnsafeUtil`（`Unsafe`对象通过反射获取）

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

![image-20230622113618388](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-56fe068439895a5c1a8842efdf349b22e97eadca%2Fimage-20230622113618388.png?alt=media)

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

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

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

![image-20230622113236211](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-873538222d69cf65e60daf86199812eae2bc3837%2Fimage-20230622113236211.png?alt=media)

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

### 反序列化流程

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

![image-20230622115616836](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-d57e9784dffb69d07287bc05270433e74de371d4%2Fimage-20230622115616836.png?alt=media)

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

![image-20230622120315235](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-0bcea4942f1aebf2120f62fe27993101de2c4b39%2Fimage-20230622120315235.png?alt=media)

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

![image-20230622120938963](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-00c63d0f1cf1b16550a2135a6c6682f5d616e711%2Fimage-20230622120938963.png?alt=media)

可以跟一下这里的`readObjectOrNull`

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

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

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

![image-20230622120815891](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-255780da5ced02988764e7f075609ca8dbc26ad4%2Fimage-20230622120815891.png?alt=media)

到此反序列化就结束了

### 总结

![image-20230622095425825](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-9bc37c1a4c7edf0179f43ca89758190f154023a8%2Fimage-20230622095425825.png?alt=media)

## 0x04 Way To Attack

网上找到的Kryo反序列化问题都是放在Dubbo那块的。Dubbo默认的序列化协议是Hessian，但可以修改Dubbo协议数据包中的header，指定SerializationID，来确定Consumer和Provider通信使用的序列化协议，这里就不细讲Dubbo数据包的修改了，而是抽取其中关键的Kryo反序列化，Dubbo相关的具体可以看[这里](https://p4d0rn.gitbook.io/java/serial-journey/other-components/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`

![image-20230622125024956](https://1239337109-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2HLPOkuOfb7iyCzDJ8vA%2Fuploads%2Fgit-blob-ea047c478d28a2624cc5789fe594462a36fda5ba%2Fimage-20230622125024956.png?alt=media)

`org.springframework.aop.target.HotSwappableTargetSource#equals`

```java
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()`函数，进而调用`JSONSerializer`的`write()`函数，从而触发Fastjson Gadget

## 0x05 Reference

* [浅析Dubbo Kryo/FST反序列化漏洞（CVE-2021-25641）Mi1k7ea](https://www.mi1k7ea.com/2021/06/30/%E6%B5%85%E6%9E%90Dubbo-KryoFST%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2021-25641%EF%BC%89/)
* [Dubbo的反序列化安全问题——kryo和fst - bitterz - 博客园 (cnblogs.com)](https://www.cnblogs.com/bitterz/p/15588955.html)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://p4d0rn.gitbook.io/java/serial-journey/other-components/kryo.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
