Merge pull request #5811 from mogwailabs/insecureJmxRmiServerEnvironment

Java: Add query - insecure environment configuration during JMX/RMI server init
This commit is contained in:
Chris Smowton
2021-06-25 22:09:20 +01:00
committed by GitHub
17 changed files with 424 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.rmi.registry.LocateRegistry;
import java.util.HashMap;
import java.util.Map;
import javax.management.MBeanServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
public class CorrectJmxInitialisation {
public void initAndStartJmxServer() throws IOException{
int jmxPort = 1919;
LocateRegistry.createRegistry(jmxPort);
/* Restrict the login function to String Objects only (see CVE-2016-3427) */
Map<String, Object> env = new HashMap<String, Object>();
// For Java 10+
String stringsOnlyFilter = "java.lang.String;!*"; // Deny everything but java.lang.String
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, stringsOnlyFilter);
/* Java 9 or below:
env.put("jmx.remote.rmi.server.credential.types",
new String[] { String[].class.getName(), String.class.getName() });
*/
MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer();
JMXServiceURL jmxUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + jmxPort + "/jmxrmi");
// Create JMXConnectorServer in a secure manner
javax.management.remote.JMXConnectorServer connectorServer = JMXConnectorServerFactory
.newJMXConnectorServer(jmxUrl, env, beanServer);
connectorServer.start();
}
}

View File

@@ -0,0 +1,32 @@
public class CorrectRmiInitialisation {
public void initAndStartRmiServer(int port, String hostname, boolean local) {
MBeanServerForwarder authzProxy = null;
env.put("jmx.remote.x.daemon", "true");
/* Restrict the login function to String Objects only (see CVE-2016-3427) */
Map<String, Object> env = new HashMap<String, Object>();
// For Java 10+
String stringsOnlyFilter = "java.lang.String;!*"; // Deny everything but java.lang.String
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, stringsOnlyFilter);
/* Java 9 or below
env.put("jmx.remote.rmi.server.credential.types",
new String[] { String[].class.getName(), String.class.getName() });
*/
int rmiPort = Integer.getInteger("com.sun.management.jmxremote.rmi.port", 0);
RMIJRMPServerImpl server = new RMIJRMPServerImpl(rmiPort,
(RMIClientSocketFactory) env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE),
(RMIServerSocketFactory) env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE), env);
JMXServiceURL serviceURL = new JMXServiceURL("rmi", hostname, rmiPort);
// Create RMI Server
RMIConnectorServer jmxServer = new RMIConnectorServer(serviceURL, env, server,
ManagementFactory.getPlatformMBeanServer());
jmxServer.start();
}
}

View File

@@ -0,0 +1,60 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>For special use cases some applications may implement a custom service which handles JMX-RMI connections.</p>
<p>When creating such a custom service, a developer should pass a certain environment configuration to the JMX-RMI server initalisation,
as otherwise the JMX-RMI service is susceptible to an unsafe deserialization vulnerability.</p>
<p>This is because the JMX-RMI service allows attackers to supply arbitrary objects to the service authentication
method, resulting in the attempted deserialization of an attacker-controlled object.
In the worst case scenario this could allow an attacker to achieve remote code execution within the context of the application server.</p>
<p>By setting the appropriate environment, the deserialization can be controlled via a deserialization filter.</p>
</overview>
<recommendation>
<p>During the creation of a custom JMX-RMI service an environment should be supplied that sets a deserialization filter.
Ideally this filter should be as restrictive as possible, for example to only allow the deserialization of <code>java.lang.String</code>.</p>
<p>The filter can be configured by setting the key <code>jmx.remote.rmi.server.credentials.filter.pattern</code> (given by the constant <code>RMIConnectorServer.CREDENTIALS_FILTER_PATTERN</code>).
The filter should (ideally) only allow java.lang.String and disallow all other classes for deserialization: (<code>"java.lang.String;!*"</code>).</p>
<p>The key-value pair can be set as following:</p>
<sample src="example_filter_java_10.java" />
<p>For applications using Java 6u113 to 9:</p>
<sample src="example_filter_java_9.java" />
<p>Please note that the JMX-RMI service is vulnerable in the default configuration.
For this reason an initialization with a <code>null</code> environment is also vulnerable.</p>
</recommendation>
<example>
<p>The following examples show how an JMX-RMI service can be initialized securely.</p>
<p>The first example shows how an JMX server is initialized securely with the <code>JMXConnectorServerFactory.newJMXConnectorServer()</code> call.</p>
<sample src="CorrectJMXConnectorServerFactoryEnvironmentInitialisation.java" />
<p>The second example shows how a JMX Server is initialized securely if the <code>RMIConnectorServer</code> class is used.</p>
<sample src="CorrectRMIConnectorServerEnvironmentInitalisation.java" />
</example>
<references>
<li>Deserialization of arbitrary objects could lead to remote code execution as described following: <a href="https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data">OWASP Deserialization of untrusted data</a>.</li>
<li>Issue discovered in Tomcat (CVE-2016-8735): <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-8735">OWASP ESAPI</a>.</li>
<li><a href="https://www.oracle.com/java/technologies/javase/8u91-relnotes.html#bugfixes-8u91">Oracle release notes</a>: New attribute for JMX RMI JRMP servers.</li>
<li>Java 10 API specification for <a href="https://docs.oracle.com/javase/10/docs/api/javax/management/remote/rmi/RMIConnectorServer.html#CREDENTIALS_FILTER_PATTERN">RMIConnectorServer.CREDENTIALS_FILTER_PATTERN</a></li>
<li>The Java API specification for <a href="https://docs.oracle.com/javase/10/docs/api/javax/management/remote/rmi/RMIConnectorServer.html#CREDENTIAL_TYPES">RMIConnectorServer.CREDENTIAL_TYPES</a>. Please note that this field is deprecated since Java 10.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,90 @@
/**
* @name InsecureRmiJmxAuthenticationEnvironment
* @description This query detects if a JMX/RMI server is created with a potentially dangerous environment, which could lead to code execution through insecure deserialization.
* @kind problem
* @problem.severity error
* @tags security
* external/cwe/cwe-665
* @precision high
* @id java/insecure-rmi-jmx-server-initialization
*/
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.Maps
/** Holds if `constructor` instantiates an RMI or JMX server. */
predicate isRmiOrJmxServerCreateConstructor(Constructor constructor) {
constructor
.getDeclaringType()
.hasQualifiedName("javax.management.remote.rmi", "RMIConnectorServer")
}
/** Holds if `method` creates an RMI or JMX server. */
predicate isRmiOrJmxServerCreateMethod(Method method) {
method.getName() = "newJMXConnectorServer" and
method.getDeclaringType().hasQualifiedName("javax.management.remote", "JMXConnectorServerFactory")
}
/**
* Models flow from the qualifier of a
* `map.put("jmx.remote.rmi.server.credential.types", value)` call
* to an RMI or JMX initialisation call.
*/
class SafeFlow extends DataFlow::Configuration {
SafeFlow() { this = "MapToPutCredentialstypeConfiguration" }
override predicate isSource(DataFlow::Node source) { putsCredentialtypesKey(source.asExpr()) }
override predicate isSink(DataFlow::Node sink) {
exists(Call c |
isRmiOrJmxServerCreateConstructor(c.getCallee()) or
isRmiOrJmxServerCreateMethod(c.getCallee())
|
sink.asExpr() = c.getArgument(1)
)
}
/**
* Holds if a `put` call on `qualifier` puts a key match
* into the map.
*/
private predicate putsCredentialtypesKey(Expr qualifier) {
exists(MapPutCall put |
put.getKey().(CompileTimeConstantExpr).getStringValue() =
[
"jmx.remote.rmi.server.credential.types",
"jmx.remote.rmi.server.credentials.filter.pattern"
]
or
put.getKey()
.(FieldAccess)
.getField()
.hasQualifiedName("javax.management.remote.rmi", "RMIConnectorServer",
["CREDENTIAL_TYPES", "CREDENTIALS_FILTER_PATTERN"])
|
put.getQualifier() = qualifier and
put.getMethod().(MapMethod).getReceiverKeyType() instanceof TypeString and
put.getMethod().(MapMethod).getReceiverValueType() instanceof TypeObject
)
}
}
/** Gets a string describing why the application is vulnerable, depending on if the vulnerability is present due to a) a null environment b) an insecurely set environment map */
string getRmiResult(Expr e) {
// We got a Map so we have a source and a sink node
if e instanceof NullLiteral
then
result =
"RMI/JMX server initialized with a null environment. Missing type restriction in RMI authentication method exposes the application to deserialization attacks."
else
result =
"RMI/JMX server initialized with insecure environment $@, which never restricts accepted client objects to 'java.lang.String'. This exposes to deserialization attacks against the RMI authentication method."
}
from Call c, Expr envArg
where
(isRmiOrJmxServerCreateConstructor(c.getCallee()) or isRmiOrJmxServerCreateMethod(c.getCallee())) and
envArg = c.getArgument(1) and
not any(SafeFlow conf).hasFlowToExpr(envArg)
select c, getRmiResult(envArg), envArg, envArg.toString()

View File

@@ -0,0 +1,4 @@
String stringsOnlyFilter = "java.lang.String;!*"; // Deny everything but java.lang.String
Map<String, Object> env = new HashMap<String, Object>;
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, stringsOnlyFilter);

View File

@@ -0,0 +1,9 @@
// This is deprecated in Java 10+ !
Map<String, Object>; env = new HashMap<String, Object>;
env.put (
"jmx.remote.rmi.server.credential.types",
new String[]{
String[].class.getName(),
String.class.getName()
}
);

View File

@@ -0,0 +1,4 @@
| InsecureRmiJmxEnvironmentConfiguration.java:12:5:12:69 | newJMXConnectorServer(...) | RMI/JMX server initialized with a null environment. Missing type restriction in RMI authentication method exposes the application to deserialization attacks. | InsecureRmiJmxEnvironmentConfiguration.java:12:59:12:62 | null | null |
| InsecureRmiJmxEnvironmentConfiguration.java:17:5:17:50 | new RMIConnectorServer(...) | RMI/JMX server initialized with a null environment. Missing type restriction in RMI authentication method exposes the application to deserialization attacks. | InsecureRmiJmxEnvironmentConfiguration.java:17:34:17:37 | null | null |
| InsecureRmiJmxEnvironmentConfiguration.java:25:5:25:49 | new RMIConnectorServer(...) | RMI/JMX server initialized with insecure environment $@, which never restricts accepted client objects to 'java.lang.String'. This exposes to deserialization attacks against the RMI authentication method. | InsecureRmiJmxEnvironmentConfiguration.java:25:34:25:36 | env | env |
| InsecureRmiJmxEnvironmentConfiguration.java:33:5:33:68 | newJMXConnectorServer(...) | RMI/JMX server initialized with insecure environment $@, which never restricts accepted client objects to 'java.lang.String'. This exposes to deserialization attacks against the RMI authentication method. | InsecureRmiJmxEnvironmentConfiguration.java:33:59:33:61 | env | env |

View File

@@ -0,0 +1,89 @@
import java.io.IOException;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.rmi.RMIConnectorServer;
import java.util.HashMap;
import java.util.Map;
public class InsecureRmiJmxEnvironmentConfiguration {
public void initInsecureJmxDueToNullEnv() throws IOException {
// Bad initializing env (arg1) with null
JMXConnectorServerFactory.newJMXConnectorServer(null, null, null);
}
public void initInsecureRmiDueToNullEnv() throws IOException {
// Bad initializing env (arg1) with null
new RMIConnectorServer(null, null, null, null);
}
public void initInsecureRmiDueToMissingEnvKeyValue() throws IOException {
// Bad initializing env (arg1) with missing
// "jmx.remote.rmi.server.credential.types"
Map<String, Object> env = new HashMap<>();
env.put("jmx.remote.x.daemon", "true");
new RMIConnectorServer(null, env, null, null);
}
public void initInsecureJmxDueToMissingEnvKeyValue() throws IOException {
// Bad initializing env (arg1) with missing
// "jmx.remote.rmi.server.credential.types"
Map<String, Object> env = new HashMap<>();
env.put("jmx.remote.x.daemon", "true");
JMXConnectorServerFactory.newJMXConnectorServer(null, env, null);
}
public void secureJmxConnnectorServer() throws IOException {
// Good
Map<String, Object> env = new HashMap<>();
env.put("jmx.remote.x.daemon", "true");
env.put("jmx.remote.rmi.server.credential.types",
new String[] { String[].class.getName(), String.class.getName() });
JMXConnectorServerFactory.newJMXConnectorServer(null, env, null);
}
public void secureRmiConnnectorServer() throws IOException {
// Good
Map<String, Object> env = new HashMap<>();
env.put("jmx.remote.x.daemon", "true");
env.put("jmx.remote.rmi.server.credential.types",
new String[] { String[].class.getName(), String.class.getName() });
new RMIConnectorServer(null, env, null, null);
}
public void secureeJmxConnectorServerConstants1() throws IOException {
// Good
Map<String, Object> env = new HashMap<>();
env.put("jmx.remote.x.daemon", "true");
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, "java.lang.String;!*"); // Deny everything but
// java.lang.String
JMXConnectorServerFactory.newJMXConnectorServer(null, env, null);
}
public void secureeRmiConnectorServerConstants1() throws IOException {
// Good
Map<String, Object> env = new HashMap<>();
env.put("jmx.remote.x.daemon", "true");
String stringsOnlyFilter = "java.lang.String;!*"; // Deny everything but java.lang.String
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, stringsOnlyFilter);
new RMIConnectorServer(null, env, null, null);
}
public void secureJmxConnectorServerConstants2() throws IOException {
// Good
Map<String, Object> env = new HashMap<>();
env.put("jmx.remote.x.daemon", "true");
env.put("jmx.remote.rmi.server.credentials.filter.pattern", "java.lang.String;!*"); // Deny everything but
// java.lang.String
JMXConnectorServerFactory.newJMXConnectorServer(null, env, null);
}
public void secureRmiConnectorServerConstants2() throws IOException {
// Good
Map<String, Object> env = new HashMap<>();
env.put("jmx.remote.x.daemon", "true");
String stringsOnlyFilter = "java.lang.String;!*"; // Deny everything but java.lang.String
env.put("jmx.remote.rmi.server.credentials.filter.pattern", stringsOnlyFilter);
new RMIConnectorServer(null, env, null, null);
}
}

View File

@@ -0,0 +1 @@
experimental/Security/CWE/CWE-665/InsecureRmiJmxEnvironmentConfiguration.ql

View File

@@ -0,0 +1 @@
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/rmi-remote-0.0.0

View File

@@ -0,0 +1,30 @@
package javax.management.remote.rmi;
import java.io.IOException;
import java.util.Map;
import java.io.IOException;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXServiceURL;
import javax.management.MBeanServer;
import javax.management.remote.rmi.RMIServerImpl;
//import javax.management.remote.JMXConnectorServer;
//public class RMIConnectorServerTEST extends JMXConnectorServer{
public class RMIConnectorServer extends java.lang.Object {
public static final String CREDENTIALS_FILTER_PATTERN = "jmx.remote.rmi.server.credentials.filter.pattern";
public RMIConnectorServer(JMXServiceURL url, Map<String, ?> environment) throws IOException {
// stub;
}
public RMIConnectorServer(JMXServiceURL url, Map<String, ?> environment, MBeanServer mbeanServer)
throws IOException {
// stub;
}
public RMIConnectorServer(JMXServiceURL url, Map<String, ?> environment, RMIServerImpl rmiServerImpl,
MBeanServer mbeanServer) throws IOException {
// stub;
}
}

View File

@@ -0,0 +1,10 @@
package javax.management.remote.rmi;
import java.util.Map;
public class RMIServerImpl {
public RMIServerImpl(Map<String, ?> env) {
// stub;
}
}

View File

@@ -0,0 +1 @@
This is a workaround for a bug in which the extractor can't resolve type javax.management.remote.rmi.RMIConnectorServer even though it has been part of the JDK since Java 5

View File

@@ -0,0 +1,6 @@
package javax.management.remote.rmi;
import java.rmi.Remote;
import java.io.Closeable;
interface RMIConnection extends Closeable, Remote { }

View File

@@ -0,0 +1,34 @@
package javax.management.remote.rmi;
import java.util.Map;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.MBeanServerForwarder;
import javax.management.MBeanServer;
// Note this is a partial stub sufficient to the needs of tests for CWE-665
public class RMIConnectorServer extends JMXConnectorServer {
public RMIConnectorServer(JMXServiceURL url, Map<String,?> environment) { }
public RMIConnectorServer(JMXServiceURL url, Map<String,?> environment, MBeanServer mbeanServer) { }
public RMIConnectorServer(JMXServiceURL url, Map<String,?> environment, RMIServerImpl rmiServerImpl, MBeanServer mbeanServer) { }
public static String CREDENTIAL_TYPES = "";
public static String CREDENTIALS_FILTER_PATTERN = "";
public static String JNDI_REBIND_ATTRIBUTE = "";
public static String RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE = "";
public static String RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE = "";
public static String SERIAL_FILTER_PATTERN = "";
public Map<String,?> getAttributes() { return null; }
public JMXServiceURL getAddress() { return null; }
public String[] getConnectionIds() { return null; }
public boolean isActive() { return true; }
public void setMBeanServerForwarder(MBeanServerForwarder mbsf) { }
public void start() { }
public void stop() { }
public JMXConnector toJMXConnector(Map<String,?> env) { return null; }
}

View File

@@ -0,0 +1,3 @@
package javax.management.remote.rmi;
interface RMIServer { }

View File

@@ -0,0 +1,12 @@
package javax.management.remote.rmi;
import java.io.Closeable;
import java.rmi.Remote;
public class RMIServerImpl implements Closeable, RMIServer {
public void close() { }
public String getVersion() { return null; }
public RMIConnection newClient(Object credentials) { return null; }
}