学习大纲
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 1. 概念 1.1 . 序列化与反序列化 1.2 . 为何要进行序列化和反序列 1.3 . 反序列化为何产生的安全问题 1.4 . 序列化的好处 1.5 . 序列化和反序列的应用场景 2. 序列化与反序列化代码实现 2.1 . 代码展示 2.1 .1 . 类文件:Person.java 2.1 .2 . 序列化文件:SerializtionTest.java 2.1 .3 . 反序列化文件:UnserializeTest.java 3. ObjectOutputStream 3.1 . ObjectOutputStream继承的父类和实现的接口 3.1 .1 . writeObject 3.1 .2 . writeObject0() 4. ObjectInputStream 4.1 . ObjectInputStream继承的父类和实现的接口 4.1 .1 . readObject 5. 序列化与反序列化代码解析 5.1 . 基本实现 5.1 .1 . SerializationTest.java 5.1 .2 . UnserializationTest.java 5.2 . Serializable接口 5.2 .1 . 1. 序列化类在没有实现Serializable接口时在进行序列化时会报错 5.2 .2 . 2. 静态成员变量是不能被序列化的 5.2 .3 . 3. transient 标识的对象成员变量不参与序列化 6. 反序列化为什么会产生安全问题 6.1 . 入口类的readObject直接调用恶意代码 6.1 .1 . 通过debug探究readObject是如何执行恶意代码的 6.1 .1 .1 . 1. 跟进到readObject() 6.1 .1 .2 . 2. readObject()调用readObject0() 6.1 .1 .3 . 3. 继续调用readOrdinaryObject() 6.1 .1 .4 . 4. 调用readSerialDate() 6.1 .1 .5 . 5. 调用invokeReadObject() 6.1 .2 . debug过程中的源码分析 6.1 .2 .1 . 1. readObject() 6.1 .2 .2 . 2. readObject0() 6.1 .2 .3 . 3. readOrdinaryObject() 6.1 .2 .4 . 4. readSerialDate() 6.1 .2 .5 . 5. invokeReadObject() 6.1 .3 . tips 7. 攻击路线 7.1 . 要满足的一些条件 7.2 . 如何找到入口类 7.2 .1 . HashMap 7.2 .1 .1 . 1. 继承Serializable接口 7.2 .1 .2 . 2. 包含重写的readObject()方法 7.2 .1 .3 . 3. 调用常见的函数
概念
在开始之前想聊一些关于概念的东西,比如何为反序列化,为什么要进行反序了化,序列化的好处和什么情况下需要使用到序列化和反序列化和其为什么会产生安全问题。
序列化与反序列化
在Java
中的序列化与反序列化,就是将一个Java
对象当前的状态(即为该对象的属性)以字符串(即为字节序列)的形式描述出来;然后该字符串可能会被传递or存储到某个地方,在我们需要的时候在将这串字节序列还原为原来的Java
对象。在两者的转换之间需要一条规则;规则中描述了序列化和反序列化时如何将对象转换成字节序列,又如何将字节序列转换成对象。因为这两者之间是可逆的。
所以综上所述,Java
序列化就是将Java
对象转换为字节序列;其中ObjectOutputStream
的writeObject()
方法可以实现序列化。Java
反序列化就是将字节序列转换为Java
对象,其中ObjectInputStream
的readObject()
方法可以实现序列化。
为何要进行序列化和反序列
当两个进程进行通讯时我们知道其可以互相传递各种数据,包括文本、图片、音频、视频等;这些数据都会以二进制的形式在网络中传输。而当两个Java
进程进行通讯时,同样可以实现两个对象的传输。这其中是如何实现的,这就需要Java
的序列化和反序列化了;发送方需要把这个Java
对象进行序列化转换为字节序列进行传输,在接收方收到这个字节序列时需要对其进行反序列化将其还原为原来的Java
对象。
反序列化为何产生的安全问题
只要服务器在接受到字节序列然后对他进行反序列化时,客户端传递类的readObject()
中的代码会自动执行,这就给予了攻击者可以构造恶意代码然后使其在服务器上运行从而达到攻击效果。
序列化的好处
1.能够实现数据的持久化,通过序列化可以把数据永久的保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。
2.利用序列化实现远程通信,在网络上传送对象的字节序列。
序列化和反序列的应用场景
想把内存中的对象保存到一个文件中或者是数据库当中。
用套接字在网络上传输对象。
通过 RMI 传输对象的时候。
序列化与反序列化代码实现
代码展示
类文件:Person.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.io.Serializable;public class Person implements Serializable { private String name; private int age; public Person () { } public Person (String name,int age) { this .name = name; this .age = age; } @Override public String toString () { return "Person{" + "name = '" + name + '\'' + ",age = " + age + '}' ; } }
序列化文件:SerializtionTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutput;import java.io.ObjectOutputStream;public class SerializationTest { public static void serialize (Object obj) throws IOException{ ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static void main (String[] args) throws Exception{ Person person = new Person ("hey" ,22 ); System.out.println(person); serialize(person); } }
反序列化文件:UnserializeTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.io.FileInputStream;import java,io,IOException;import java.io.ObjcetInputStream;public class UnserializeTest { public static Object unserialize (String File) throws OException,ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream (new FileInputStream (Filename)); Object obj = ois.readObject(); return obj; } public static void main (String[] args) throws Exception{ Person person = (Person)unserialize("ser.bin" ); System.out.println(person); } }
ObjectOutputStream
1 2 3 4 5 6 7 8 9 10 11 12 13 public class ObjectOutputStream extends OutputStream implements ObjectOutput , ObjectStreamConstants { private static class Caches { static final ConcurrentMap<WeakClassKey,Boolean> subclassAudits = new ConcurrentHashMap <>(); static final ReferenceQueue<Class<?>> subclassAuditsQueue = new ReferenceQueue <>(); }
ObjectOutputStream
继承的父类和实现的接口
父类 :OutputStream
;所有字节输出流的顶级父类,用来接收输出的字节并发送到某些接收器(sink)。
接口 :ObjectOutput
接口扩展了DataOutput
接口,DataOutput
接口提供了将任何Java
对象转换成二进制流的功能。而ObjectOutput
在其基础上提供了writeObject
方法,也就是类的写入
接口 :ObjectStreamConstants
定义了一些在对象序列化时写入的常量。常见的一些的比如 STREAM_MAGIC
、STREAM_VERSION
等。
通过这个类继承的父类和实现的接口我们可以得知这个类主要实现的功能就是将Java
对象的一系列特征转换成可输出流,也就是我们所说的序列化。
writeObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public final void writeObject (Object obj) throws IOException { if (enableOverride) { writeObjectOverride(obj); return ; } try { writeObject0(obj, false ); } catch (IOException ex) { if (depth == 0 ) { writeFatalException(ex); } throw ex; } }
writeObject
是ObjectOutputStream
的主要方法之一;其功能即为将Java
对象写入输出流。任何对象,包括字符串和数组,都是用 writeObject
写入到流中的。
之前说过,序列化的过程,就是将一个对象当前的状态描述为字节序列的过程,也就是 Object -> OutputStream
的过程,这个过程由 writeObject
实现。writeObject
方法负责为指定的类编写其对象的状态,以便在后面可以使用与之对应 readObject
方法来恢复它。
writeObject0()
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 private void writeObject0 (Object obj, boolean unshared) throws IOException { boolean oldMode = bout.setBlockDataMode(false ); depth++; try { int h; if ((obj = subs.lookup(obj)) == null ) { writeNull(); return ; } else if (!unshared && (h = handles.lookup(obj)) != -1 ) { writeHandle(h); return ; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return ; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return ; } Object orig = obj; Class<?> cl = obj.getClass(); ObjectStreamClass desc; for (;;) { Class<?> repCl; desc = ObjectStreamClass.lookup(cl, true ); if (!desc.hasWriteReplaceMethod() || (obj = desc.invokeWriteReplace(obj)) == null || (repCl = obj.getClass()) == cl) { break ; } cl = repCl; } if (enableReplace) { Object rep = replaceObject(obj); if (rep != obj && rep != null ) { cl = rep.getClass(); desc = ObjectStreamClass.lookup(cl, true ); } obj = rep; } if (obj != orig) { subs.assign(orig, obj); if (obj == null ) { writeNull(); return ; } else if (!unshared && (h = handles.lookup(obj)) != -1 ) { writeHandle(h); return ; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return ; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return ; } } if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException ( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException (cl.getName()); } } } finally { depth--; bout.setBlockDataMode(oldMode); } }
writeObject
和 writeUnshared
实际上调用 writeObject0
方法,也就是说 writeObject0
是上面两个方法的基础实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ObjectInputStream extends InputStream implements ObjectInput , ObjectStreamConstants { private static final int NULL_HANDLE = -1 ; private static final Object unsharedMarker = new Object (); private static final HashMap<String, Class<?>> primClasses = new HashMap <>(8 , 1.0F ); static { primClasses.put("boolean" , boolean .class); primClasses.put("byte" , byte .class); primClasses.put("char" , char .class); primClasses.put("short" , short .class); primClasses.put("int" , int .class); primClasses.put("long" , long .class); primClasses.put("float" , float .class); primClasses.put("double" , double .class); primClasses.put("void" , void .class); }
父类 :InputStream
,所有字节输入流的顶级父类
接口 :ObjectInput
;ObjectInput
扩展了 DataInput
接口,DataInput
接口提供了从二进制流读取字节并将其重新转换为 Java
基础类型的功能,ObjectInput
额外提供了 readObject
方法用来读取类。
readObject
从 ObjectInputStream
读取一个对象,将会读取对象的类、类的签名、类的非 transient
和非 static
字段的值,以及其所有父类类型。
我们可以使用 writeObject
和 readObject
方法为一个类重写默认的反序列化执行方,所以其中 readObject
方法会 “传递性” 的执行,也就是说,在反序列化过程中,会调用反序列化类的 readObject
方法,以完整的重新生成这个类的对象。
序列化与反序列化代码解析
基本实现
SerializationTest.java
这里我们将代码进行封装,将序列化功能封装进serialize
这个方法中,在序列化当中我们通过FileOutputStream
输出流对象,将序列化的对象输出到ser.bin
中;接着调用oos
的writeObject()
方法,将对象进行序列化操作。
1 2 ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" ));oos.writeObject(obj);
UnserializationTest.java
进行反序列化
1 2 ObjectInputStream ois = new ObjectInputStream (new FileInputStream (Filename));Object obj = ois.readObject();
Serializable
接口
此时想聊点重要的东西;通过ObjectOutputStream
将需要序列化数据写入到流中,因为Java IO
是一种装饰者模式,因此可以通过ObjectOutStream
包装FileOutStream
将数据写入到文件中或者包装ByteArrayOutStream
将数据写入到内存中。同理,可以通过ObjectInputStream
将数据从磁盘FileInputStream
或者内存ByteArrayInputStream
读取出来然后转化为指定的对象即可。
1.序列化类在没有实现Serializable
接口时在进行序列化时会报错
1 2 3 4 5 6 7 public class Person implements Serializable {} public class Person { }
此时的Serializable
用来标识当前类可以被ObjectOutputStream
进行序列化,以及被ObjectInputStream
进行反序列化。
当我们将Serializable
删除时便会出现报错
2.静态成员变量是不能被序列化的
序列化是针对对象属性的,而静态成员属于类的。
3.transient标识的对象成员变量不参与序列化
此时我们在类文件中的成员变量里加入transient
标识
然后先进行序列化再进行反序列化可以发现再name
里面已经变成null
反序列化为什么会产生安全问题
我们知道,一个类想要实现序列化和反序列化必须实现Serializable
或Externalizable
接口;Serializable
接口是一个标记接口,标记了这类可以被序列化和反序列化;而Externalizable
接口在Serializable
的基础上又提供了writeExternal
和readExternal
,用来序列化和反序列化一些外部元素。
其中如果被序列化的类重写了writeObject
和readObject
方法;Java
会委托这两个方法来进行序列化和反序列化。而正是因为这个特性,在反序列化的类中如果重写了readObject
方法;在服务端进行反序列化时便会调用它,如果这个方法中存在一些恶意构造的代码那么便会实现攻击者的目的。
这时候我们先举一个最简单的例子来看看
入口类的readObject
直接调用恶意代码
虽然这种情况在开发中基本不可能出现,但是我们作为初学者还是老老实实的将其过一遍。
此时我们重写readObject
方法,在方法中插入恶意代码使用Runtime
执行命令弹出计算器。
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 import java.io.IOException;import java.io.Serializable;import java.io.ObjectInputStream;public class Person implements Serializable { private String name; private int age; public Person () { } public Person (String name,int age) { this .name = name; this .age = age; } @Override public String toString () { return "Person{" + "name = '" + name + '\'' + ",age = " + age + '}' ; } private void readObject (ObjectInputStream ois) throws IOException,ClassNotFoundException{ ois.defaultReadObject(); Runtime.getRuntime().exec("calc" ); } }
运行之后即可发现我们插入的恶意代码已经成功执行了,这个是我们攻击者最理想的漏洞。接下来我会通过idea
的动调来探究恶意代码的执行过程。
通过debug探究readObject
是如何执行恶意代码的
此时我们将断点下在Person person = (Person)unserialize("ser.bin");
这里
1.跟进到readObject()
此时跟进到readObject()
方法
2.readObject()
调用readObject0()
跟进去,此时发现readObject()
方法实际调用了readObject0()
方法来反序列化字符串
3.继续调用readOrdinaryObject()
此时readObject0()
方法以字节的方式去读取,如果读取到0x73
则代表这是一个反序列化数据,则会继续调用readOrdinaryObject()
方法进行处理
4.调用readSerialDate()
跟进readOrdinaryObject()
方法,此时readOrdinaryObject()
方法会调用 readClassDesc()
方法读取类描述符,并根据其中的内容判断类是否实现了Externalizable
接口,如果是,则调用 readExternalData
方法去执行反序列化类中的 readExternal
,如果不是,则调用 readSerialData()
方法去执行类中的 readObject
() 方法。
5.调用invokeReadObject()
在 readSerialData()
方法中,首先通过类描述符获得了序列化对象的数据布局。通过布局的 hasReadObjectMethod()
方法判断对象是否有重写 readObject
方法,如果有,则使用 invokeReadObject()
方法调用对象中的 readObject
。
而通过invokeReadObject()
方法调用对象中的readObject
后,因为我们已经重写了readObject
所以导致了其中恶意代码的执行
debug过程中的源码分析
书接上文,在readObject()
调用了readObject0()
来反序列化字符串后我们来看看它的源码构造
1.readObject()
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 public final Object readObject () throws IOException, ClassNotFoundException { if (enableOverride) { return readObjectOverride(); } int outerHandle = passHandle; try { Object obj = readObject0(false ); handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null ) { throw ex; } if (depth == 0 ) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0 ) { clear(); } } }
这段代码是ObjectInputStream
的核心方法之一,负责实现对象的反序列化过程,同时处理了一些可能的异常情况和嵌套读取的情况。此时其中的核心是在于Object obj = readObject0(flase)
,其调用了readObject0()
的方法从字节流中读取对象的字节表示,并反序列化为Java
对象。
2.readObject0()
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 private Object readObject0 (boolean unshared) throws IOException { boolean oldMode = bin.getBlockDataMode(); if (oldMode) { int remain = bin.currentBlockRemaining(); if (remain > 0 ) { throw new OptionalDataException (remain); } else if (defaultDataEnd) { throw new OptionalDataException (true ); } bin.setBlockDataMode(false ); } byte tc; while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); } depth++; try { switch (tc) { case TC_NULL: return readNull(); case TC_REFERENCE: return readHandle(unshared); case TC_CLASS: return readClass(unshared); case TC_CLASSDESC: case TC_PROXYCLASSDESC: return readClassDesc(unshared); case TC_STRING: case TC_LONGSTRING: return checkResolve(readString(unshared)); case TC_ARRAY: return checkResolve(readArray(unshared)); case TC_ENUM: return checkResolve(readEnum(unshared)); case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); case TC_EXCEPTION: IOException ex = readFatalException(); throw new WriteAbortedException ("writing aborted" , ex); case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true ); bin.peek(); throw new OptionalDataException ( bin.currentBlockRemaining()); } else { throw new StreamCorruptedException ( "unexpected block data" ); } case TC_ENDBLOCKDATA: if (oldMode) { throw new OptionalDataException (true ); } else { throw new StreamCorruptedException ( "unexpected end of block data" ); } default : throw new StreamCorruptedException ( String.format("invalid type code: %02X" , tc)); } } finally { depth--; bin.setBlockDataMode(oldMode); } }
首先,检查输入流当前是否处于块数据模式(block data mode)
,如果是,则根据情况抛出OptionalDataException
异常或者进行一些处理。
在一个循环中,不断读取下一个字节并判断其类型(type code)
:
如果是TC_RESET
类型,则调用handleReset()
方法进行重置处理。
根据不同的类型码(如TC_NULL、TC_REFERENCE等)
,调用相应的方法进行对象的读取和处理。
对于一些特殊的类型码,如TC_BLOCKDATA、TC_ENDBLOCKDATA
等,根据情况抛出相应的异常。
对于无法识别的类型码,抛出StreamCorruptedException
异常。
在try-catch-finally
块中,处理读取过程中可能出现的异常情况:
在finally
块中,减少深度计数并根据旧的模式恢复块数据模式。
这段代码是ObjectInputStream
的一个私有方法,用于具体实现对象的反序列化。此时我们知道readObject0()
方法以字节的方式去读,如果读到 0x73
,则代表这是一个对象的序列化数据,将会调用 readOrdinaryObject()
方法进行处理。
3.readOrdinaryObject()
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 private Object readOrdinaryObject (boolean unshared) throws IOException { if (bin.readByte() != TC_OBJECT) { throw new InternalError (); } ObjectStreamClass desc = readClassDesc(false ); desc.checkDeserialize(); Class<?> cl = desc.forClass(); if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) { throw new InvalidClassException ("invalid class descriptor" ); } Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null ; } catch (Exception ex) { throw (IOException) new InvalidClassException ( desc.forClass().getName(), "unable to create instance" ).initCause(ex); } passHandle = handles.assign(unshared ? unsharedMarker : obj); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null ) { handles.markException(passHandle, resolveEx); } if (desc.isExternalizable()) { readExternalData((Externalizable) obj, desc); } else { readSerialData(obj, desc); } handles.finish(passHandle); if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { handles.setObject(passHandle, obj = rep); } } return obj; }
readOrdinaryObject
方法会调用 readClassDesc
方法读取类描述符,并根据其中的内容判断类是否实现了 Externalizable
接口,如果是,则调用 readExternalData
方法去执行反序列化类中的 readExternal
,如果不是,则调用 readSerialData
方法去执行类中的 readObject
方法。
4.readSerialDate()
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 private void readSerialData (Object obj, ObjectStreamClass desc) throws IOException { ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0 ; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slots[i].hasData) { if (obj == null || handles.lookupException(passHandle) != null ) { defaultReadFields(null , slotDesc); } else if (slotDesc.hasReadObjectMethod()) { SerialCallbackContext oldContext = curContext; if (oldContext != null ) oldContext.check(); try { curContext = new SerialCallbackContext (obj, slotDesc); bin.setBlockDataMode(true ); slotDesc.invokeReadObject(obj, this ); } catch (ClassNotFoundException ex) { handles.markException(passHandle, ex); } finally { curContext.setUsed(); if (oldContext!= null ) oldContext.check(); curContext = oldContext; } defaultDataEnd = false ; } else { defaultReadFields(obj, slotDesc); } if (slotDesc.hasWriteObjectData()) { skipCustomData(); } else { bin.setBlockDataMode(false ); } } else { if (obj != null && slotDesc.hasReadObjectNoDataMethod() && handles.lookupException(passHandle) == null ) { slotDesc.invokeReadObjectNoData(obj); } } } }
在 readSerialData
方法中,首先通过类描述符获得了序列化对象的数据布局。通过布局的 hasReadObjectMethod
方法判断对象是否有重写 readObject
方法,如果有,则使用 invokeReadObject
方法调用对象中的 readObject
。
5.invokeReadObject()
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 void invokeReadObject (Object obj, ObjectInputStream in) throws ClassNotFoundException, IOException, UnsupportedOperationException { requireInitialized(); if (readObjectMethod != null ) { try { readObjectMethod.invoke(obj, new Object []{ in }); } catch (InvocationTargetException ex) { Throwable th = ex.getTargetException(); if (th instanceof ClassNotFoundException) { throw (ClassNotFoundException) th; } else if (th instanceof IOException) { throw (IOException) th; } else { throwMiscException(th); } } catch (IllegalAccessException ex) { throw new InternalError (ex); } } else { throw new UnsupportedOperationException (); } }
这段代码也是最终执行恶意代码的地方,这段代码的作用是调用对象的readObject
方法,通过反射来实现对对象的readObject
方法的调用,并处理可能的异常情况。如果对象没有readObject
方法,则抛出UnsupportedOperationException
异常。
tips
经过动态调试和源码审计我们已经可以捋出整个反序列化漏洞的形成原因和调用过程,此时的形成原因在于我们重写了对象的readObject()
方法,而程序又会调用这个readObject()
方法,最终导致了恶意代码的执行。而整个的调用过程如下
1 readObject() --> readObject0() --> readOrdinaryObject() --> readSerialDate() --> invokeReadObject()
攻击路线
要满足的一些条件
共同条件 :继承Serializable
接口
入口类 :kick-off
或者source
;入口点(重写了readObject
的类)(重写readObject
、参数类型宽泛、调用常见的函数、jdk
自带)
调用链 :gadget chain
;中间的调用链,相同名称、相同类型
执行类 :sink
;最终执行恶意动作的点,比如exec()
如何找到入口类
HashMap
1.继承Serializable
接口
此时最重要的前提是这个类继承了Serializable
接口
2.包含重写的readObject()
方法
此时我们可以找到HashMap
中拥有重写的readObject()
此时我们接着往下看,可以发现参数key
和value
都执行了readObject()
方法
执行了readObject()
方法之后再将这两个变量扔进hash()
函数中;此时我们跟进hash()
函数
3.调用常见的函数
此时若满足key
不为空,则会继续跳进hashCode()
此时hashCode()
位于常见的函数Object
类当中,满足调用常见函数。