(after discussing the issue with the project maintainers, agreed to publish the details to make users aware of unsafe RMI deserialization in RemoteEngine
)
I noticed that Weka offers a remote object via RMI:
|
Naming.bind(name, engine); |
The remote object has methods with not-primitive parameters:
Object executeTask(Task t) throws RemoteException;
Object checkStatus(Object taskId) throws Exception;
RMI uses the default Java serialization mechanism to pass parameters in RMI invocations. A remote attacker can send a malicious serialized object to the above RMI entries. The objects get deserialized without any check on the incoming data. In the worst case, it may let the attacker run arbitrary code remotely. You can find more details about this attack in the following articles
https://mogwailabs.de/en/blog/2019/03/attacking-java-rmi-services-after-jep-290/
https://itnext.io/java-rmi-for-pentesters-part-two-reconnaissance-attack-against-non-jmx-registries-187a6561314d
Unfortunately, I don't see any easy way to fix it because RMI does not allow restricting classes for deserialization. The only idea I have is to set a process-wide filter for deserialization, see JEP 290:
http://openjdk.java.net/jeps/290
As an example, I am attaching DeserializationConfig
that helps to set the filter. The method DeserializationConfig.setDeserializationFilterIfNecessary()
has to be called before accessing the default serialization mechanism in the JVM. It is important because the filter is loaded only once when the JVM loads classes which implement the default deserialization mechanism (ObjectInputStream
and others). For example, you can call the method in the beginning of main()
. Or, maybe you can just delete RemoteEngine
.
import java.security.Security;
/**
* This class helps to configure deserialization.
*/
public class DeserializationConfig {
/**
* This filter specifies classes that are allowed for deserialization in RMI communications.
*
* @see <a href="http://openjdk.java.net/jeps/290">JEP 290: Filter Incoming Serialization Data</a>
*/
private static final String DEFAULT_DESERIALIZATION_FILTER = String.join(";",
"weka.**",
"java.lang.Boolean", "java.lang.Byte", "java.lang.Character", "java.lang.Double", "java.lang.Enum",
"java.lang.Float", "java.lang.Integer", "java.lang.Long", "java.lang.Number", "java.lang.Object",
"java.lang.Short",
"java.util.*",
"!*"
);
/**
* Returns a process-wide deserialization filter.
*/
private static String getSerialFilter() {
String filter = System.getProperty("jdk.serialFilter");
if (filter != null) {
return filter;
}
return Security.getProperty("jdk.serialFilter");
}
/**
* Sets a process-wide deserialization filter if it is not already set.
*/
public static void setDeserializationFilterIfNecessary() {
String filter = getSerialFilter();
if (filter == null) {
System.out.printf("Use the following filter for deserialization:%n%s%n", DEFAULT_DESERIALIZATION_FILTER);
System.setProperty("jdk.serialFilter", DEFAULT_DESERIALIZATION_FILTER);
}
}
}