1.Java序列化与反序列化
序列化与反序列化对于 Java 程序员来说,应该不算陌生了,序列化与反序列化简单来说就是 Java 对象与数据之间的相互转化。
- **Java序列化:**把Java对象转换为字节序列的过程,便于保存在内存、文件、数据库中。
- **Java反序列化:**把字节序列恢复为Java对象的过程,ObjectInputStream类的readObject()方法用于反序列化。
那为什么要序列化文件呢?
实质上,序列化机制并不只局限于 Java 语言,序列化的本质是内存对象到数据流的一种转换,我们知道内存中的东西不具备持久性,但有些场景却需要将对象持久化保存或传输。例如缓存系统中存储了用户的 Session,如果缓存系统直接下线,带系统重启后用户就需要重新登陆,为了使缓存系统内存中的 Session 对象一直有效,就需要有一种机制将对象从内存中保存入磁盘,并且待系统重启后还能将 Session 对象恢复到内存中,这个过程就是对象序列化与反序列化的过程,从而避免了用户会话的有效性受系统故障的影响。
此外,在 Java 工程中,序列化还广泛应用于 JMX,RMI,网络传输(协议包对象)等场景,可以说序列化机制赋予了内存对象持久化的机会,就像虚拟机镜像(VMware Take a snapshot),也可以将序列化机制看作是内存对象的一种镜像机制。
(这里要是不懂的话可以多读几遍)
我们来简单实现以下Java序列化与反序列化之间的过程:
解释:
我们先将一句话进行序列化,然后保存为test.txt文件,然后对其进行反序列化,将其还原出来!
代码附上:
package Demo;
import java.io.*;
public class Main {
public static void main(String[] args) {
Main m = new Main();
try {
m.run();
m.run2();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public void run() throws IOException {
FileOutputStream out = new FileOutputStream("test.txt");
ObjectOutputStream obj_out = new ObjectOutputStream(out);
User u = new User();
u.setName("Java是世界上最好的语言");
obj_out.writeObject(u);
obj_out.close();
System.out.println("User对象序列化成功!");
}
public void run2() throws IOException, ClassNotFoundException {
FileInputStream in = new FileInputStream("test.txt");
ObjectInputStream ins = new ObjectInputStream(in);
User u = (User) ins.readObject();
System.out.println("User对象反序列化成功!");
System.out.println(u.getName());
ins.close();
}
}
package Demo;
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;
}
}
2.Externalizable接口
实现Externalizable接口进行序列化和反序列化,必须实现writeExternal、readExternal方法,并且还要实现一个类的无参构造方法。
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class biu implements Externalizable {
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private String name;
//实现无参构造方法
public biu(){
System.out.println(this.getClass()+"无参构造方法被调用");
}
//实现writeExternal方法
public void writeExternal(ObjectOutput out) throws IOException {}
//实现readExternal方法
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {}
}
用法和Serializable接口一样!
3.readObject()方法
这个方法在反序列化漏洞中起到了重要的作用,因为在序列化过程中,JVM虚拟机会试图调用对象类里的 writeObject()和 readObject() 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject() 方法以及 ObjectInputStream 的 defaultReadObject() 方法。
实现了Serializable接口可以执行的方法包括readObject()、readObjectNoData()、readResolve(),以及实现了Externalizable接口的readExternal()方法。这些在找反序列化漏洞时都需要重点关注。
import java.io.ObjectInputStream;
import java.io.Serializable;
public class user implements Serializable{
public user(){
System.out.println(this.getClass()+"无参构造方法被调用");
}
public user(String name){
System.out.println(this.getClass()+"user(String name)构造方法被调用");
this.name=name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString(){
System.out.println(this.getClass() + "的toString()被调用");
return "userclass{name="+getName()+"}";
}
private void readObject(ObjectInputStream in) throws Exception{
//执行默认的readObject()方法
in.defaultReadObject();
System.out.println(this.getClass() + "的readObject()被调用");
//windows重点
Runtime.getRuntime().exec(new String[]{"cmd", "/c", name});
}
private String name;
}
在readObject()方法中存在Runtime.getRuntime().exec(new String[]{“cmd”, “/c”, name});name为执行命令参数,那么我们可以构造一个恶意的对象,将name赋值为我们想要执行的命令,那么当反序列化时就可以触发readObject()造成RCE。
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
public class UserSerializable {
public static void main(String[] args) throws Exception {
user user = new user("calc");
user.setName("calc");
//序列化对象
serialize(user);
//反序列化
user user1 = unserialize();
System.out.println(user1);
}
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("序列化完成.");
}
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();
System.out.println("反序列化完成.");
return user;
}
}
可以看到在上图中不仅触发了重写的readObject()方法弹出calc程序,而且还触发了如toString()方法、setName()方法等,所以在实际寻找利用链时应该不局限于readObject()方法,其他方法中如果有执行命令的函数也可以进行利用。
当然,在实际情况中不可能会出现开发这样编写代码,这里是为了方便展示而进行编写,所以反序列化漏洞通常会需要Java的一些特性进行配合比如反射(invoke),然后就是利用链的寻找。
一般,java反序列化漏洞需要三个东西
- 反序列化入口
- 目标方法
- 利用链
补充
标志
数据以rO0AB开头,基本可以确定这串就是JAVA序列化base64加密的数据。
如果以aced开头,那么他就是这一段java序列化的16进制。
工具
ysoserial是一个生成序列化payload数据的工具。
java -jar ysoserial-0.0.4-all.jar CommonsCollections1 '想要执行的命令' > payload.out