Remote Code Execution via RMI Deserialization

Critical Risk deserialization
javarmideserializationrceremote-method-invocation

What it is

Remote code execution vulnerabilities occur when RMI methods accept non-primitive object parameters that trigger deserialization of untrusted data, enabling gadget chain exploitation for arbitrary code execution.

// Remote interfacepublic interface DataService extends Remote {    // VULNERABLE: Accepting complex object parameters    String processData(UserData data) throws RemoteException;    void updateConfiguration(Properties config) throws RemoteException;    List<String> queryResults(SearchCriteria criteria) throws RemoteException;}// Implementationpublic class DataServiceImpl implements DataService {        @Override    public String processData(UserData data) throws RemoteException {        // VULNERABLE: Object deserialization happens automatically        return "Processed data for user: " + data.getUsername();    }        @Override    public void updateConfiguration(Properties config) throws RemoteException {        // VULNERABLE: Properties object can contain dangerous payloads        System.getProperties().putAll(config);    }        @Override    public List<String> queryResults(SearchCriteria criteria) throws RemoteException {        // VULNERABLE: SearchCriteria deserialization can trigger gadgets        return Arrays.asList("result1", "result2");    }}// RMI Server setuppublic class RMIServer {    public static void main(String[] args) throws Exception {        // VULNERABLE: No deserialization filtering        DataService service = new DataServiceImpl();        DataService stub = (DataService) UnicastRemoteObject.exportObject(service, 0);                Registry registry = LocateRegistry.createRegistry(1099);        registry.bind("DataService", stub);                System.out.println("RMI Server started");    }}
// SECURE: Use primitive parameters and IDspublic interface SecureDataService extends Remote {    // SECURE: Only primitive and safe types    String processDataById(long userId, String dataType) throws RemoteException;    void updateConfigurationValue(String key, String value) throws RemoteException;    List<String> queryResultsByJson(String jsonCriteria) throws RemoteException;}// Secure implementationpublic class SecureDataServiceImpl implements SecureDataService {        private final UserRepository userRepository = new UserRepository();    private final ObjectMapper objectMapper = new ObjectMapper();        @Override    public String processDataById(long userId, String dataType) throws RemoteException {        // SECURE: Fetch data server-side using primitive ID        UserData data = userRepository.findById(userId);        if (data != null) {            return "Processed " + dataType + " for user: " + data.getUsername();        }        return "User not found";    }        @Override    public void updateConfigurationValue(String key, String value) throws RemoteException {        // SECURE: Validate individual key-value pairs        if (isValidConfigKey(key) && isValidConfigValue(value)) {            System.setProperty(key, value);        } else {            throw new RemoteException("Invalid configuration parameter");        }    }        @Override    public List<String> queryResultsByJson(String jsonCriteria) throws RemoteException {        try {            // SECURE: Parse JSON instead of deserializing objects            SearchCriteria criteria = objectMapper.readValue(jsonCriteria, SearchCriteria.class);            return performQuery(criteria);        } catch (Exception e) {            throw new RemoteException("Invalid search criteria", e);        }    }        private boolean isValidConfigKey(String key) {        // Allowlist of valid configuration keys        Set<String> allowedKeys = Set.of("app.timeout", "app.maxConnections", "app.debug");        return allowedKeys.contains(key);    }        private boolean isValidConfigValue(String value) {        // Basic validation for configuration values        return value != null && value.length() < 100 && !value.contains("<script>");    }        private List<String> performQuery(SearchCriteria criteria) {        // Perform safe query based on validated criteria        return Arrays.asList("result1", "result2");    }}// SECURE: RMI Server with deserialization filtering (JEP 290)public class SecureRMIServer {        public static void main(String[] args) throws Exception {        // SECURE: Set up deserialization filter        System.setProperty("jdk.serialFilter", createSecureSerialFilter());                // Alternative: Programmatic filter        ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(createSecureSerialFilter());        ObjectInputFilter.Config.setSerialFilter(filter);                SecureDataService service = new SecureDataServiceImpl();        SecureDataService stub = (SecureDataService) UnicastRemoteObject.exportObject(service, 0);                Registry registry = LocateRegistry.createRegistry(1099);        registry.bind("SecureDataService", stub);                System.out.println("Secure RMI Server started with deserialization filtering");    }        private static String createSecureSerialFilter() {        return "java.lang.String;" +               "java.lang.Number;" +               "java.util.List;" +               "java.util.ArrayList;" +               "com.example.safe.SearchCriteria;" +               "!*"; // Reject all other classes    }}// SECURE: Alternative approach with custom socket factoriespublic class FilteredRMIServer {        public static void main(String[] args) throws Exception {        // SECURE: Use custom socket factory with filtering        RMIServerSocketFactory serverFactory = new FilteredServerSocketFactory();        RMIClientSocketFactory clientFactory = new FilteredClientSocketFactory();                SecureDataService service = new SecureDataServiceImpl();        SecureDataService stub = (SecureDataService) UnicastRemoteObject.exportObject(            service, 0, clientFactory, serverFactory);                Registry registry = LocateRegistry.createRegistry(1099, clientFactory, serverFactory);        registry.bind("FilteredDataService", stub);                System.out.println("RMI Server with filtered sockets started");    }}// Custom server socket factory with deserialization protectionclass FilteredServerSocketFactory implements RMIServerSocketFactory {    @Override    public ServerSocket createServerSocket(int port) throws IOException {        return new ServerSocket(port) {            @Override            public Socket accept() throws IOException {                Socket socket = super.accept();                return new FilteredSocket(socket);            }        };    }}class FilteredSocket extends Socket {    private final Socket delegate;        public FilteredSocket(Socket delegate) {        this.delegate = delegate;    }        @Override    public InputStream getInputStream() throws IOException {        return new ObjectInputStream(delegate.getInputStream()) {            @Override            protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {                ObjectStreamClass desc = super.readClassDescriptor();                if (!isAllowedClass(desc.getName())) {                    throw new InvalidClassException("Class not allowed: " + desc.getName());                }                return desc;            }        };    }        private boolean isAllowedClass(String className) {        Set<String> allowedClasses = Set.of(            "java.lang.String",            "java.lang.Integer",            "java.lang.Long",            "com.example.safe.SearchCriteria"        );        return allowedClasses.contains(className);    }}

💡 Why This Fix Works

The vulnerable code was updated to address the security issue.

Why it happens

Running RMI servers without ObjectInputFilter, allowing deserialization of arbitrary classes from remote clients.

Root causes

RMI Without Serialization Filters

Running RMI servers without ObjectInputFilter, allowing deserialization of arbitrary classes from remote clients.

Classpath Gadget Chains

Having vulnerable libraries (Commons Collections, Spring) in classpath that provide RCE gadget chains.

Legacy RMI Services

Maintaining older RMI services that predate Java serialization filter mechanisms.

Fixes

1

Implement ObjectInputFilter

Use ObjectInputFilter with strict allowlists to control which classes can be deserialized in RMI communication.

2

Replace RMI with REST/gRPC

Migrate from Java RMI to modern alternatives like REST APIs or gRPC that don't use Java serialization.

3

Enable JEP 290 Filters

Configure JVM-wide deserialization filters using jdk.serialFilter system property to block dangerous classes.

Detect This Vulnerability in Your Code

Sourcery automatically identifies remote code execution via rmi deserialization and many other security issues in your codebase.