Java反序列化漏洞防范:如何使用白名单机制限制可反序列化的类

Java 反序列化漏洞防范:白名单机制深度解析

各位朋友,大家好!今天我们来深入探讨一个非常重要的安全问题:Java 反序列化漏洞,以及如何利用白名单机制进行有效防御。

反序列化漏洞是 Java 应用中一个常见的安全威胁,它允许攻击者通过构造恶意序列化数据,在应用程序中执行任意代码。如果不加以防范,可能会导致数据泄露、服务中断甚至系统完全被控制。

白名单机制是一种有效的防御手段,通过明确指定允许反序列化的类,从而阻止恶意类的反序列化。今天,我们将深入了解白名单机制的原理、实现方式以及在实际应用中的注意事项。

一、反序列化漏洞原理回顾

在深入白名单机制之前,我们先简单回顾一下反序列化漏洞的原理。

Java 的序列化机制可以将对象转换为字节流,以便于存储或传输。反序列化则是将字节流还原为对象的过程。这个过程本身没有问题,问题在于反序列化过程中,对象的状态会被恢复,如果字节流中包含了恶意构造的对象,那么在反序列化过程中,可能会触发恶意代码的执行。

以下是一个简单的反序列化漏洞示例:

import java.io.*;

public class VulnerableClass implements Serializable {
    private String command;

    public VulnerableClass(String command) {
        this.command = command;
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        Runtime.getRuntime().exec(command); // 执行命令
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 序列化
        VulnerableClass obj = new VulnerableClass("calc");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        oos.close();

        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        VulnerableClass recoveredObj = (VulnerableClass) ois.readObject();
        ois.close();
    }
}

在这个例子中,VulnerableClassreadObject 方法在反序列化时会执行 command 变量指定的命令。如果攻击者能够控制 command 变量的值,就可以执行任意命令。

漏洞成因总结:

  • readObject 方法: 反序列化过程中,readObject 方法会被自动调用,攻击者可以利用这个方法执行恶意代码。
  • 对象状态恢复: 反序列化会恢复对象的状态,包括敏感数据,如果字节流被篡改,可能会导致数据泄露。
  • 类型不安全: ObjectInputStream 默认情况下允许反序列化任何类型的对象,这为攻击者提供了可乘之机。

二、白名单机制:核心思想与策略

白名单机制的核心思想是:只允许反序列化明确信任的类,拒绝所有未知的类。

这是一种“默认拒绝”的安全策略,与“默认允许”的策略相比,它更加安全可靠。因为在“默认允许”的策略下,任何未知的类都可以被反序列化,而攻击者可能会利用这些未知类来执行恶意代码。

白名单策略的关键点:

  • 明确性: 白名单中的类必须明确指定,不能使用通配符或其他模糊匹配方式。
  • 最小化: 白名单中的类应该尽可能少,只包含应用程序真正需要反序列化的类。
  • 可维护性: 白名单应该易于维护和更新,以便在应用程序发生变化时及时调整。

白名单机制的优势:

  • 有效性: 可以有效阻止恶意类的反序列化,降低安全风险。
  • 可控性: 可以精确控制哪些类可以被反序列化,提高应用程序的安全性。
  • 易于实施: 可以通过多种方式实现,例如自定义 ObjectInputStream 或使用第三方库。

三、白名单机制的实现方式

现在,我们来探讨几种常见的白名单机制实现方式。

1. 自定义 ObjectInputStream

最直接的方式是自定义 ObjectInputStream,并重写其 resolveClass 方法。resolveClass 方法在反序列化过程中会被调用,用于加载类。我们可以通过在这个方法中进行类名检查,实现白名单过滤。

import java.io.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class WhiteListObjectInputStream extends ObjectInputStream {

    private final Set<String> whiteList;

    public WhiteListObjectInputStream(InputStream in, Set<String> whiteList) throws IOException {
        super(in);
        this.whiteList = new HashSet<>(whiteList); // 使用 HashSet 提高查找效率
    }

    public WhiteListObjectInputStream(InputStream in, String... whiteList) throws IOException {
        super(in);
        this.whiteList = new HashSet<>(Arrays.asList(whiteList));
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        String className = desc.getName();
        if (!whiteList.contains(className)) {
            throw new SecurityException("Unauthorized deserialization attempt: " + className);
        }
        return super.resolveClass(desc);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 定义白名单
        Set<String> allowedClasses = new HashSet<>();
        allowedClasses.add("java.lang.String");
        allowedClasses.add("com.example.MyObject"); // 假设有一个类叫 MyObject

        // 序列化 MyObject
        MyObject obj = new MyObject("Hello, world!");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        oos.close();

        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        WhiteListObjectInputStream ois = new WhiteListObjectInputStream(bis, allowedClasses);
        MyObject recoveredObj = (MyObject) ois.readObject();
        ois.close();

        System.out.println(recoveredObj.getMessage());

        // 尝试反序列化一个不在白名单中的类
        try {
            ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
            ObjectOutputStream oos2 = new ObjectOutputStream(bos2);
            oos2.writeObject(new java.util.ArrayList<>()); // ArrayList 不在白名单中
            oos2.close();

            ByteArrayInputStream bis2 = new ByteArrayInputStream(bos2.toByteArray());
            WhiteListObjectInputStream ois2 = new WhiteListObjectInputStream(bis2, allowedClasses);
            ois2.readObject();
            ois2.close();

        } catch (SecurityException e) {
            System.out.println("SecurityException caught: " + e.getMessage());
        }

    }
}

class MyObject implements Serializable {
    private String message;

    public MyObject(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

代码解释:

  • WhiteListObjectInputStream 继承自 ObjectInputStream
  • 构造函数接收一个 whiteList 参数,用于存储允许反序列化的类名。
  • resolveClass 方法首先获取要加载的类名,然后检查该类名是否在白名单中。
  • 如果类名不在白名单中,则抛出一个 SecurityException 异常,阻止反序列化。
  • main 方法演示了如何使用 WhiteListObjectInputStream 进行反序列化,以及如何处理 SecurityException 异常。

优点:

  • 简单易懂,易于实现。
  • 可以精确控制哪些类可以被反序列化。

缺点:

  • 需要手动维护白名单,容易出错。
  • 如果应用程序依赖于大量的类,维护白名单的成本会很高。
  • 对于嵌套的类,需要递归地检查白名单,增加了复杂性。

2. 使用第三方库

有一些第三方库提供了更高级的反序列化安全功能,例如:

  • SerialKiller: SerialKiller 是一个轻量级的 Java 反序列化防火墙,它可以阻止已知的高危漏洞利用链,并提供白名单和黑名单功能。
  • SafeSerialization: SafeSerialization 是另一个 Java 反序列化安全库,它提供了多种防御机制,包括白名单、黑名单、类型验证和深度限制。

以 SerialKiller 为例,演示如何使用白名单:

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class SerialKillerExample {

    public static void main(String[] args) throws Exception {
        // 1. 配置 Kryo 实例并设置白名单
        Kryo kryo = new Kryo();
        Set<Class<?>> whiteList = new HashSet<>(Arrays.asList(
                String.class, // 允许 String 类型
                MyObject.class // 允许 MyObject 类型
        ));
        kryo.setDefaultSerializer(new com.esotericsoftware.kryo.serializers.JavaSerializer());// 使用Java序列化器
        whiteList.forEach(kryo::register); // 注册白名单中的类

        // 2. 序列化 MyObject 对象
        MyObject obj = new MyObject("Hello from SerialKiller!");
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        Output output = new Output(outputStream);
        kryo.writeObject(output, obj);
        output.close();
        byte[] serializedData = outputStream.toByteArray();

        // 3. 反序列化 MyObject 对象
        ByteArrayInputStream inputStream = new ByteArrayInputStream(serializedData);
        Input input = new Input(inputStream);
        MyObject deserializedObj = kryo.readObject(input, MyObject.class);
        input.close();

        System.out.println("Deserialized message: " + deserializedObj.getMessage());

        // 4. 尝试反序列化一个不在白名单中的类 (List)
        try {
            ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream();
            Output output2 = new Output(outputStream2);
            kryo.writeObject(output2, Arrays.asList("item1", "item2")); //尝试序列化List
            output2.close();
            byte[] serializedData2 = outputStream2.toByteArray();

            ByteArrayInputStream inputStream2 = new ByteArrayInputStream(serializedData2);
            Input input2 = new Input(inputStream2);
            kryo.readObject(input2, Arrays.asList("").getClass()); // 尝试反序列化
            input2.close();

        } catch (Exception e) {
            System.out.println("Exception caught: " + e.getMessage()); // Kryo 会抛出异常
        }
    }

    static class MyObject {
        private String message;

        public MyObject(String message) {
            this.message = message;
        }

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }

        @Override
        public String toString() {
            return "MyObject{" +
                    "message='" + message + ''' +
                    '}';
        }
    }
}

代码解释:

  • 首先,创建一个 Kryo 实例,它是 SerialKiller 提供的序列化/反序列化工具。
  • 定义一个白名单 whiteList,其中包含了允许反序列化的类 StringMyObject
  • 使用 kryo.register() 方法将白名单中的类注册到 Kryo 实例中。
  • 序列化和反序列化 MyObject 对象。
  • 尝试反序列化一个不在白名单中的类 java.util.List,SerialKiller 会抛出异常,阻止反序列化。

优点:

  • 提供了更高级的功能,例如白名单、黑名单、类型验证和深度限制。
  • 可以自动阻止已知的高危漏洞利用链。
  • 易于配置和使用。

缺点:

  • 需要引入第三方库,增加了项目的依赖。
  • 可能需要学习第三方库的使用方法。

3. JVM 级别的反序列化过滤器 (Java 9+)

Java 9 引入了 JVM 级别的反序列化过滤器,可以通过配置 jdk.serialFilter 系统属性或使用 java.io.ObjectInputFilter API 来限制可以反序列化的类。

使用 jdk.serialFilter 系统属性:

可以在启动 JVM 时设置 jdk.serialFilter 系统属性,指定允许或禁止反序列化的类。

例如:

java -Djdk.serialFilter="!*Evil*,java.lang.*,java.util.*"  MyApp

这个命令表示:

  • 禁止反序列化类名包含 "Evil" 的类。
  • 允许反序列化 java.langjava.util 包下的类。

使用 java.io.ObjectInputFilter API:

可以使用 java.io.ObjectInputFilter API 在代码中动态地配置反序列化过滤器。

import java.io.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class ObjectInputFilterExample {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 1. 定义白名单
        Set<String> allowedClasses = new HashSet<>(Arrays.asList(
                "java.lang.String",
                "com.example.MyObject"
        ));

        // 2. 创建 ObjectInputFilter
        ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
                String.join(";", allowedClasses.stream().map(className -> className + "=accept").toArray(String[]::new)) + ";reject"
        );

        // 3. 序列化 MyObject
        MyObject obj = new MyObject("Hello from ObjectInputFilter!");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        oos.close();

        // 4. 反序列化 MyObject 并设置过滤器
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        ois.setObjectInputFilter(filter); // 设置过滤器
        MyObject recoveredObj = (MyObject) ois.readObject();
        ois.close();

        System.out.println("Deserialized message: " + recoveredObj.getMessage());

        // 5. 尝试反序列化一个不在白名单中的类 (ArrayList)
        try {
            ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
            ObjectOutputStream oos2 = new ObjectOutputStream(bos2);
            oos2.writeObject(new java.util.ArrayList<>());
            oos2.close();

            ByteArrayInputStream bis2 = new ByteArrayInputStream(bos2.toByteArray());
            ObjectInputStream ois2 = new ObjectInputStream(bis2);
            ois2.setObjectInputFilter(filter);
            ois2.readObject();
            ois2.close();
        } catch (Exception e) {
            System.out.println("Exception caught: " + e.getMessage()); // 过滤器会阻止反序列化
        }
    }

    static class MyObject implements Serializable {
        private String message;

        public MyObject(String message) {
            this.message = message;
        }

        public String getMessage() {
            return message;
        }
    }
}

代码解释:

  • 首先,定义一个白名单 allowedClasses,其中包含了允许反序列化的类 java.lang.Stringcom.example.MyObject
  • 使用 ObjectInputFilter.Config.createFilter() 方法创建一个 ObjectInputFilter 对象。过滤器的规则字符串使用分号分隔,每个规则由类名和动作组成,例如 java.lang.String=accept 表示允许反序列化 java.lang.String 类,reject 表示拒绝所有未明确允许的类。
  • 在反序列化之前,使用 ois.setObjectInputFilter(filter) 方法将过滤器设置到 ObjectInputStream 对象上。
  • 尝试反序列化一个不在白名单中的类 java.util.ArrayList,过滤器会阻止反序列化,并抛出异常。

优点:

  • JVM 级别的支持,性能更高。
  • 配置灵活,可以使用系统属性或 API 进行配置。
  • 可以设置更复杂的过滤规则,例如限制类的深度、大小等。

缺点:

  • 只能在 Java 9 及以上版本中使用。
  • 配置规则比较复杂,需要仔细学习。

4. 比较表格

特性 自定义 ObjectInputStream 第三方库 (SerialKiller) JVM 级别的反序列化过滤器
易用性 简单 较简单 复杂
功能 基本的白名单 高级的白名单、黑名单等 高级的过滤规则
性能 较好 较好 最好
依赖 无 (Java 9+)
适用场景 简单的白名单需求 复杂的安全需求 复杂的安全需求,Java 9+
漏洞防御能力 基本
维护成本 较高 中等 中等

四、白名单机制的注意事项

在实际应用中,使用白名单机制需要注意以下几点:

  • 白名单的范围: 白名单应该尽可能小,只包含应用程序真正需要反序列化的类。过度宽泛的白名单可能会降低安全性。
  • 白名单的维护: 白名单需要定期维护和更新,以适应应用程序的变化。每次修改应用程序时,都需要检查白名单是否需要更新。
  • 默认类的处理: Java 中有一些默认的类,例如 java.lang.Stringjava.lang.Integer 等,这些类经常被使用,可以考虑添加到白名单中。
  • 内部类的处理: 如果需要反序列化内部类,需要将外部类和内部类都添加到白名单中。
  • 继承关系的处理: 如果允许反序列化一个类,那么它的所有子类也都可以被反序列化。如果只需要允许反序列化特定的子类,需要明确指定。
  • 与黑名单结合使用: 可以与黑名单结合使用,进一步提高安全性。例如,可以先使用黑名单阻止已知的高危漏洞利用链,然后再使用白名单允许反序列化特定的类。
  • 安全审计: 定期进行安全审计,检查白名单的配置是否正确,是否存在安全风险。

五、代码示例:白名单与黑名单结合使用

import java.io.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class WhiteAndBlackListObjectInputStream extends ObjectInputStream {

    private final Set<String> whiteList;
    private final Set<String> blackList;

    public WhiteAndBlackListObjectInputStream(InputStream in, Set<String> whiteList, Set<String> blackList) throws IOException {
        super(in);
        this.whiteList = new HashSet<>(whiteList);
        this.blackList = new HashSet<>(blackList);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        String className = desc.getName();

        // 先检查黑名单
        if (blackList.contains(className)) {
            throw new SecurityException("Class is blacklisted: " + className);
        }

        // 再检查白名单
        if (!whiteList.contains(className)) {
            throw new SecurityException("Class is not whitelisted: " + className);
        }

        return super.resolveClass(desc);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 定义白名单
        Set<String> allowedClasses = new HashSet<>(Arrays.asList(
                "java.lang.String",
                "com.example.MyObject"
        ));

        // 定义黑名单 (例如,禁止反序列化 Commons Collections 的某些类,因为它们存在已知的反序列化漏洞)
        Set<String> forbiddenClasses = new HashSet<>(Arrays.asList(
                "org.apache.commons.collections.functors.ChainedTransformer",
                "org.apache.commons.collections.functors.ConstantTransformer"
        ));

        // 序列化 MyObject
        MyObject obj = new MyObject("Hello from White & Black List!");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        oos.close();

        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        WhiteAndBlackListObjectInputStream ois = new WhiteAndBlackListObjectInputStream(bis, allowedClasses, forbiddenClasses);
        MyObject recoveredObj = (MyObject) ois.readObject();
        ois.close();

        System.out.println("Deserialized message: " + recoveredObj.getMessage());

        // 尝试反序列化一个不在白名单中的类
        try {
            ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
            ObjectOutputStream oos2 = new ObjectOutputStream(bos2);
            oos2.writeObject(new java.util.ArrayList<>()); // ArrayList 不在白名单中
            oos2.close();

            ByteArrayInputStream bis2 = new ByteArrayInputStream(bos2.toByteArray());
            WhiteAndBlackListObjectInputStream ois2 = new WhiteAndBlackListObjectInputStream(bis2, allowedClasses, forbiddenClasses);
            ois2.readObject();
            ois2.close();

        } catch (SecurityException e) {
            System.out.println("SecurityException caught (Whitelist): " + e.getMessage());
        }

        // 尝试反序列化一个在黑名单中的类 (这里为了演示,假设 MyObject 在黑名单中)
        try {
            Set<String> allowedClasses2 = new HashSet<>(Arrays.asList("java.lang.String")); // 只有 String 在白名单
            Set<String> forbiddenClasses2 = new HashSet<>(Arrays.asList("com.example.MyObject"));// MyObject 在黑名单
            ByteArrayOutputStream bos3 = new ByteArrayOutputStream();
            ObjectOutputStream oos3 = new ObjectOutputStream(bos3);
            oos3.writeObject(obj); // 序列化 MyObject
            oos3.close();

            ByteArrayInputStream bis3 = new ByteArrayInputStream(bos3.toByteArray());
            WhiteAndBlackListObjectInputStream ois3 = new WhiteAndBlackListObjectInputStream(bis3, allowedClasses2, forbiddenClasses2);
            ois3.readObject();
            ois3.close();

        } catch (SecurityException e) {
            System.out.println("SecurityException caught (Blacklist): " + e.getMessage());
        }
    }

    static class MyObject implements Serializable {
        private String message;

        public MyObject(String message) {
            this.message = message;
        }

        public String getMessage() {
            return message;
        }
    }
}

在这个例子中,WhiteAndBlackListObjectInputStream 同时维护了白名单和黑名单。在 resolveClass 方法中,首先检查黑名单,如果类名在黑名单中,则抛出一个 SecurityException 异常。然后,检查白名单,如果类名不在白名单中,也抛出一个 SecurityException 异常。

六、更安全的反序列化替代方案

在某些情况下,可以考虑使用更安全的反序列化替代方案,例如:

  • JSON: JSON 是一种轻量级的数据交换格式,它没有像 Java 序列化那样复杂的反序列化机制,因此更加安全。
  • Protocol Buffers: Protocol Buffers 是 Google 开发的一种高效的序列化协议,它具有良好的性能和安全性。
  • Avro: Avro 是 Apache Hadoop 的一个子项目,它也是一种高效的序列化协议,具有良好的兼容性和可扩展性。

这些替代方案通常需要重新设计应用程序的数据交换方式,但可以有效地避免反序列化漏洞。

防御反序列化漏洞的关键点

防御 Java 反序列化漏洞需要从多个方面入手,包括:使用白名单机制、避免使用默认的反序列化、使用更安全的序列化协议等。选择合适的防御方案取决于应用程序的具体需求和安全风险。

今天的分享就到这里,希望能够帮助大家更好地理解和应用白名单机制,提高 Java 应用程序的安全性。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注