java代码审计11之反序列化基础学习

之前的文章,
php代码审计15.1之反序列化

php代码审计15.2之Session与反序列化

php代码审计15.3之phar伪协议与反序列化

1、 序列化与反序列化

序列化是让Java对象脱离Java运⾏环境的⼀种⼿段,
可以有效的实现多平台之间的通信、对象持久化存储。

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

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

2、序列化与反序列化案例

2.1、使用idea生成代码与serialVersionUID

在java中 序列化 必须实现 Serializable接⼝ 序列化保存的只是对象的状态,

并不包含⽅法和静态成员变量

新建⼀个user实现 Serializable接⼝

新建一个项目,
在这里插入图片描述

使用idea生成构造方法;代码--生成--构造函数--选择所有参数

在这里插入图片描述

其实可以直接右击,选择生成

在这里插入图片描述

类似的给所有的属性生成get/set方法

在这里插入图片描述

在生成一个toString方法,

toString方法将类当作字符串使用时,就会自动调用,

在这里插入图片描述

此时可以安装一个插件,直接搜索“GenerateSerialVersio”

之后就可以直接生成函数反序列化/序列号的uid

在这里插入图片描述

稍微补充下,

serialVersionUID是Java中的一个类属性,用于在序列化和反序列化过程中进行核验的一个版本号。

它是一个唯一的整数,用于标识类的版本信息。

如果两个类具有相同的serialVersionUID,则它们可以相互序列化和反序列化。

如果serialVersionUID不同,则会抛出NotSerializableException异常。 

最终的完整代码,user.java

import java.io.Serializable;

public class user implements Serializable {
    private static final long serialVersionUID = -2831602267920455150L;
    private int age;
    private String username;
    private String password;

    public user(int age, String username, String password) {
        this.age = age;
        this.username = username;
        this.password = password;
    }

    public int getAge() {
        return age;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "user{" +
                "age=" + age +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

2.2、实例化对象

新建一个main,

实例化上边的user类,这个输出的本质就是调用的user类的toString方法,

在这里插入图片描述
main代码,

package com.example.demo2;

public class main {

    public static void main(String[] args) {
        user user = new user(20,"xbb","123456");
        System.out.println(user);
    }
}

2.3、序列化对象

main.java

package com.example.demo2;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class main {

    public static void main(String[] args) throws Exception {
        user user = new user(20,"xbb","123456");
//        System.out.println(user);
        serialize(user);
    }

    public static void serialize(Object obj) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static void unserialize() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
        user user = (user)ois.readObject();
        System.out.println(user);
    }
}

序列化后的文件是一个二进制文件,以txt的模式打开会乱码

在这里插入图片描述

以二进制的模式打开就好了,然后对比下设置serialVersionUID与不设置的区别,

红色的ser1.bin是设置的,紫色的是没有设置的(不手动设置会自动生成),

然后具体的区别也使用紫色的线划了出来,

还可以看到的是,其实序列化后保存的内容其实很有限,

在这里插入图片描述

2.4、反序列化

使用同一个serialVersionUID序列化的文件,然后反序列化是一切正常

在这里插入图片描述

但是假设反序列化读取的文件内的serialVersionUID与当前类的serialVersionUID不一致就会报错,

在这里插入图片描述

当我们注释掉类中手动设置的serialVersionUID在反序列化就一切正常

在这里插入图片描述

3、稍微深入serialVersionUID

开始说过,java的对象在序列化的时候,保存的内容有限。

很多内容不会保存,经过测试得到的结论是,序列化保存的内容如下:
	● 类名
	● 包名
	● 父类名称(未测试,猜测存在)
	● 变量名称、类型、值

其他的待测试,然后这个serialVersionUID的作用就是确认反序列化的类和代码是同一个类,

不是同一个类的话,就会报错。
当我们使用自定义的serialVersionUID时,

可以看到我们修改了toString方法,但是序列化没有保存,所以我们修改了方法,

在反序列化的时候也不会报错,

在这里插入图片描述

这还一个注意的点是,
又定义了一个新的变量“xbb”,但是序列化的内容是没有这个值的,理论是会报错的,
但是这里没有报错这里个人的猜测是因为没有校验的原因是serialVersionUID是我们自己指定的,
而具体变量的存在和值是根据serialVersionUID判断,所以没有报错,

我们使用系统自己生成的serialVersionUID来还原就会报错,
这也验证了我们上边的猜想,

在这里插入图片描述

又一个新的问题是,这个serialVersionUID的值,系统是根据什么生成的呢?

这里笔者去问了下AI,得到的回答是(下边括号里的内容是笔者测试得到的),

● 类名
● 包名
● 父类名称
● 字段名称和类型(没有值)
● 方法名称和参数类型
继续测试,既然我们可以通过指定serialVersionUID来欺骗java,

生成了一个新的变量和值没有报错,



一个场景是,

指定serialVersionUID来测试,代码在序列化的时候,

xbb变量是一个int类型的数字,内容为“111”,

我本地构造xbb的类型为字符串,内容也是“111”,会不会报错呢?



先模拟生成一个字符串的xbb,序列化保存文件的名称为“ser2.bin”

这里的背景是serialVersionUID是指定的,

在这里插入图片描述

是报错的,

在这里插入图片描述

这说明,我们通过指定serialVersionUID可以骗过系统检测,肯定是后续在还原的过程出错了,

通过二进制打开ser2.bin,猜测是这个“t..”是字符串的含义,

然后java程序在还原的时候发现序列化的内容与代码中变量类型不同导致的报错,

在这里插入图片描述

但是发现数字类型没有在bin文件中找到,是不是20太小了,来一个大的数尝试且不以“0”结尾,

但是结果还是没有在bin文件中找到,


还有就是xbb变量因为是int值,在序列化后的文件内也么有找到,

在这里插入图片描述

综上小结,

反序列化文件内的serialVersionUID必须与代码内的值一致

序列化仅仅会保存类的一些基本属性(类名,包名,变量类型和值;但是不会保存方法)

在指定serialVersionUID属性的情况下,可以在序列化的时候,少序列化一些变量,

无论是否指定serialVersionUID属性,都可以修改变量的值,但是都不能修改变量的类型

serialVersionUID系统自动生成的话,根据以下属性

	● 类名
	● 包名
	● 父类名称
	● 字段名称和类型(没有值)
	● 方法名称和参数类型

4、transient 作⽤

Java中的transient关键字,transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象	的

序列化处理过程中会被忽略。因此,transient变量不会贯穿对象的序列化和反序列化,⽣命周期仅存于

调⽤者的内存中⽽不会写到磁盘⾥进⾏持久化。
比如这里,我们使用transient来修饰xbb变量,

反序列化后的代码就没有输出“333”

在这里插入图片描述
user.java

package com.example.demo2;

import java.io.Serializable;

public class user implements Serializable {
    private static final long serialVersionUID = -2831602267920455150L;
    private int age;
    private String username;
    private String password;



    public user(int age, String username, String password) {
        this.age = age;
        this.username = username;
        this.password = password;
    }

    public int getAge() {
        return age;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
    transient public int xbb = 333;
    @Override
    public String toString() {
        return "user{" +
                "age=" + age +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}' + xbb +"0000000666";
    }
}

main.java

package com.example.demo2;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class main {

    public static void main(String[] args) throws Exception {
        user user = new user(2000566666,"xbb11","123456");
        System.out.println(user);
        unserialize();
        //serialize(user);
    }

    public static void serialize(Object obj) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser4.bin"));
        oos.writeObject(obj);
    }
    public static void unserialize() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser4.bin"));
        user user = (user)ois.readObject();
        System.out.println(user);
    }
}

5、反序列化漏洞

模拟重写 readObject 函数造成问题,


当你将一个对象序列化为字节流时,它的状态(即数据)被写入到一个输出流中。

而在反序列化过程中,readObject()方法的作用正是从一个源输入流中读取字节序列,

再把它们反序列化为一个对象,并将其返回,readObject()是可以重写的,可以定制反序列化的一些行为。



当作者重写了readObject函数,且在函数内存在一些危险的操作时,就可能会造成问题,

因为重写后的readObject函数就和php的魔术函数一样,会在反序列化的时候自动执行,
比如下边的代码,

开发本来的想法可能是弹出一个记事本,

但是恶意者可以先在本地序列化生成的时候,将cmd变量的值改为弹出计算器,

然后将本地序列化生成的文件让目标服务器执行,

就会弹出计算器,而非记事本

在这里插入图片描述
过程,

1、本地生成恶意序列化文件XX.bin

2、让目标服务器反序列化XX.bin

user.java

package com.example.demo2;

import java.io.Serializable;

public class user implements Serializable {
//    private static final long serialVersionUID = -2831602267920455150L;
    private int age;
    private String username;
    private String password;



    public user(int age, String username, String password) {
        this.age = age;
        this.username = username;
        this.password = password;
    }

    public int getAge() {
        return age;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
    transient public int xbb = 333;
    @Override
    public String toString() {
        return "user{" +
                "age=" + age +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}' + xbb +"0000000666";
    }


    public String cmd = "notepad";
    //public String cmd = "calc";

    //重写readObject()⽅法
    private void readObject(java.io.ObjectInputStream in) throws Exception {
        //执⾏默认的readObject()⽅法
        in.defaultReadObject();
        //执⾏打开计算器程序命令
        Runtime.getRuntime().exec(cmd);
    }


}

main.java

package com.example.demo2;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class main {

    public static void main(String[] args) throws Exception {
        user user = new user(2000566666,"xbb11","123456");
        System.out.println(user);
        //serialize(user);
        unserialize();

    }

    public static void serialize(Object obj) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser4.bin"));
        oos.writeObject(obj);
    }
    public static void unserialize() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser4.bin"));
        user user = (user)ois.readObject();
        System.out.println(user);
    }
}

6、反序列化漏洞的理解

以下内容为笔者个人理解,欢迎各位大佬沟通交流

6.1、项目代码中,没有重写 readObject 方法,且反序列化的参数可控,可以造成漏洞吗

网上大多的教程都是写一个demo类,demo类的 readObject 方法被重写,

其中包含了一些危害操作,比如命令执行。那么一个问题是,假设反序列化的参数是可控的,

但是该项目中没有demo类或者demo类中的readObject方法没有被重写,那么还可以造成漏洞吗,

这种情况下也是可以的,这个时候我们就需要找一些Java核心类库内,或者加载/引入的第三方库中,

有没有重写readObject方法(其中还有一些相对危险的功能)的类,这就引申出各种“利用链”
	基于java原生的库是有一个“dnslog”利用链,其中的HashMap类就重写了readObject,
	
	会出发URL的DNS解析,这就可以配合DNSLog进行Java反序列化漏洞的探测,
	
	但是仅仅可以用来进行dnslog探测,不能进一步的利用。
	
 	小结优点是作为java原生库自带的类,不受版本限制,可以用来探测是否可能存在反序列化漏洞,
 	
  	缺点是不能进一步利用
	比较常见的还有cc1利用链,
	
	CommonsCollections1利用链就是通过利用反序列化时InvokerTransformer的readObject方法中会调用transform()方法,
	
	造成远程命令执行的结果。这种就是需要测试的项目导入了CommonsCollections1库,

类似的,根据不同的框架/java代码库,还有很多的利用链,比如cc2,cc3等等

后续,会随机选择一些利用链进行解析,

6.2、只有重写 readObject方法 才会造成反序列化漏洞吗

另外,还一个问题是,上述我们都是说有类重写了 readObject方法,

那么仅仅是重写readObject方法才会造成的漏洞吗,实际上,除了readObject方法,
还有很多的方法可以造成漏洞,

比如readResolve、writeReplace、readExternal和writeExternal等
	readObject:
 			在反序列化时,readObject 会自动被调用,用于读取序列化的对象,从而从流中恢复对象。

	readResolve:
 			反序列化对象后,为产生一个质量完全确定的对象,会调用此方法,
 			用于返回一个替代的对象。但这个过程如果不当,可能产生安全隐患。

	writeReplace:
 			序列化对象前,为产生一个完全独立的新对象,会调用此方法。
 			同样,不当的处理可能产生安全隐患。

	readExternal和 writeExternal:
 			它们是Externalizable接口的一部分,该接口提供了writeExternal和readExternal两个方法,
			这个接口当被类实现的时候,类的对象序列化与反序列化将不再是由JVM自动完成,
			而是由这两个方法完成,同样的,这样的机制也可能被滥用,并引发序列化漏洞。

6.3、小结反序列化漏洞

通过以上,我们可以知道,反序列化漏洞是很多语言都存在的“通用漏洞”。

在编程语言中,某些特殊的函数或方法(比如PHP的魔法方法和Java的readObject、readResolve等方法)

在对象序列化和反序列化过程中会被自动调用。

如果这些方法的实现中存在漏洞或者安全隐患,那么就可能在反序列化过程中被利用,从而引发反序列化漏洞。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

划水的小白白

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

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

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

打赏作者

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

抵扣说明:

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

余额充值