学无止境,参考着资料边学边记录吧。
0x01 Java序列化与反序列化
Java序列化是指把Java对象转换为字节序列的过程。这一过程将数据分解成字节流,以便存储在文件中或在网络上传输;
Java反序列化是指把字节序列恢复为Java对象的过程。就是打开字节流并重构成对象,恢复数据。
序列化与反序列化都可以理解为“写”和“读”操作 ,通过以下这两个方法可以将对象实例进行“序列化”与“反序列化”操作。
1 2 3 4 5
| private void writeObject(java.io.ObjectOutputStream out)
private void readObject(java.io.ObjectInputStream in)
|
0x02 为什么需要序列化与反序列化
当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。而当两个Java进程进行通信时,可以通过Java的序列化与反序列化在进程之间直接传送对象,换句话说,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;接收方需要从字节序列中恢复出Java对象。
使用场景:
一些应用场景涉及到将对象转化成二进制,序列化保证了能够成功读取到保存的对象。
总之,序列化的用途就是传递和存储。
0x03 序列化实现的方式
3.1 Serializable
将要序列化的类实现 Serializabel 接口(Serializable接口是一个标记接口,不用实现任何方法。一旦实现了此接口,则表明该类的对象就是可序列化的),而且所有属性必须是可序列化的,就是如果一个可序列化的类的成员不是基本类型,也不是String类型,比如自己自定义的类,那这个引用类型也必须是可序列化的,否则,会导致此类不能序列化(用transient关键字修饰的属性除外,不参与序列化过程) 。
需要序列化的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package yhy;
import java.io.Serializable;
public class User implements Serializable { private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; } }
|
序列化
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
| package yhy;
import java.io.FileOutputStream; import java.io.ObjectOutputStream;
public class UserSerializable {
public static void main(String[] args) throws Exception { User user = new User(); user.setName("yhy");
serialize(user);
}
public static void serialize(User user) throws Exception { FileOutputStream fout = new FileOutputStream("user.ser"); ObjectOutputStream out = new ObjectOutputStream(fout); out.writeObject(user); out.close(); fout.close(); System.out.println("序列化完成."); }
}
|
可以看到运行后,生产了一个文件,将user 对象变成了可持久化存储的二进制数据。

可以来看一下该对象序列化后的二进制数据

序列化的数据流以魔术数字和版本号开头,这个值是在调用ObjectOutputStream序列化时,由writeStreamHeader方法写入。开头的几位一般来当作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 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package yhy;
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream;
public class UserSerializable {
public static void main(String[] args) throws Exception { User user = new User(); user.setName("yhy");
User user1 = unserialize(); System.out.println(user1.getName());
}
public static User unserialize() throws Exception { FileInputStream fileIn = new FileInputStream("user.ser"); ObjectInputStream in = new ObjectInputStream(fileIn); User user = (User) in.readObject(); in.close(); fileIn.close(); return user; } }
|

读取了序列化文件,将二进制文件重新恢复为user对象,对象里面的属性也是完美恢复。
3.2 Externalizable
通过实现Externalizable接口进行序列化和反序列胡,但必须实现writeExternal、readExternal方法,并且还要实现一个类的无参构造方法,Serializable 接口可以不用实现。
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
| package yhy;
import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput;
public class Evil implements Externalizable {
public Evil() { System.out.println(this.getClass() + "的EvilClass()无参构造方法被调用!!!!!!"); }
@Override public void writeExternal(ObjectOutput out) throws IOException {
}
@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { }
}
|
用法和实现了Serializable接口一样,这里就不演示了。
0x04 readObject()方法
特地提到这个方法是因为在反序列化漏洞中它起到了关键作用。因为在序列化过程中,JVM虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。
Java反序列化的过程中可以自动执行序列化类的四个方法,实现了Serializable接口可以执行的方法包括readObject、readObjectNoData、readResolve,以及实现了Externalizable接口的readExternal方法。这些在找反序列化漏洞时都需要重点关注。
如果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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| package yhy;
import java.io.ObjectInputStream; import java.io.Serializable;
public class EvilClass implements Serializable { String name;
public EvilClass() { System.out.println(this.getClass() + "的EvilClass()无参构造方法被调用!!!!!!"); }
public EvilClass(String name) { System.out.println(this.getClass() + "的EvilClass(String name)构造方法被调用!!!!!!"); this.name = name; }
public String getName() { System.out.println(this.getClass() + "的getName被调用!!!!!!"); return name; }
public void setName(String name) { System.out.println(this.getClass() + "的setName被调用!!!!!!"); this.name = name; }
@Override public String toString() { System.out.println(this.getClass() + "的toString()被调用!!!!!!"); return "EvilClass{" + "name='" + getName() + '\'' + '}'; }
private void readObject(ObjectInputStream in) throws Exception { in.defaultReadObject(); System.out.println(this.getClass() + "的readObject()被调用!!!!!!");
Runtime.getRuntime().exec(new String[]{"open", "-a", name}); } }
|
readObject中存在执行命令的代码Runtime.getRuntime().exec(new String[]{"open", "-a", name}),name参数是要执行的命令。那么我们可以构造一个恶意的对象,将其name属性赋值为要执行的命令,当反序列化触发readObject时就会RCE。如下
序列化和反序列化
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
| package yhy;
import java.io.*;
public class EvilSerialize { public static void main(String[] args) throws Exception { EvilClass evilObj = new EvilClass();
evilObj.setName("Calculator");
byte[] bytes = serializeToBytes(evilObj); EvilClass o = (EvilClass)deserializeFromBytes(bytes); System.out.println(o); }
public static byte[] serializeToBytes(final Object obj) throws Exception { final ByteArrayOutputStream out = new ByteArrayOutputStream(); final ObjectOutputStream objOut = new ObjectOutputStream(out); objOut.writeObject(obj); objOut.flush(); objOut.close(); return out.toByteArray(); }
public static Object deserializeFromBytes(final byte[] serialized) throws Exception { final ByteArrayInputStream in = new ByteArrayInputStream(serialized); final ObjectInputStream objIn = new ObjectInputStream(in); return objIn.readObject(); } }
|

这是一个极端的例子,在真实场景中,不会有人真的这样直接写一句执行命令的代码在readObject()中,这样写的开发绝对会被拉出去祭天的。所以反序列化漏洞通常会需要Java的一些特性进行配合比如反射(invoke)。然后就是利用链的寻找。反序列化漏洞需要三个东西
- 反序列化入口(source)
- 目标方法(sink)
- 利用链(gadget chain)
大佬们基本都会去寻找重写了这个readObject方法的类,并配合Java的invoke反射机制,构造利用链,形成了Java中最具特色的反序列化攻击。而且再看上图中的输出结果,不仅仅触发了readObject方法,还触发了toString()、无参构造、set、get方法,那么在实际寻找利用链的过程中就不仅仅需要关注readObject()的方法了。
代码地址:https://github.com/yhy0/JavaSerializeDemo
0x05 参考
Java反序列之从萌新到菜鸟 https://www.kingkk.com/2019/01/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E4%B9%8B%E4%BB%8E%E8%90%8C%E6%96%B0%E5%88%B0%E8%8F%9C%E9%B8%9F/
Java反序列化技术分享 https://github.com/Y4er/WebLogic-Shiro-shell