Skip to content

Commit 8aa9cd5

Browse files
authored
Merge pull request #5811 from mogwailabs/insecureJmxRmiServerEnvironment
Java: Add query - insecure environment configuration during JMX/RMI server init
2 parents def4a23 + e5fa532 commit 8aa9cd5

17 files changed

+424
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import java.io.IOException;
2+
import java.lang.management.ManagementFactory;
3+
import java.rmi.registry.LocateRegistry;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
7+
import javax.management.MBeanServer;
8+
import javax.management.remote.JMXConnectorServerFactory;
9+
import javax.management.remote.JMXServiceURL;
10+
11+
public class CorrectJmxInitialisation {
12+
13+
public void initAndStartJmxServer() throws IOException{
14+
int jmxPort = 1919;
15+
LocateRegistry.createRegistry(jmxPort);
16+
17+
/* Restrict the login function to String Objects only (see CVE-2016-3427) */
18+
Map<String, Object> env = new HashMap<String, Object>();
19+
// For Java 10+
20+
String stringsOnlyFilter = "java.lang.String;!*"; // Deny everything but java.lang.String
21+
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, stringsOnlyFilter);
22+
23+
/* Java 9 or below:
24+
env.put("jmx.remote.rmi.server.credential.types",
25+
new String[] { String[].class.getName(), String.class.getName() });
26+
*/
27+
28+
MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer();
29+
30+
JMXServiceURL jmxUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + jmxPort + "/jmxrmi");
31+
32+
// Create JMXConnectorServer in a secure manner
33+
javax.management.remote.JMXConnectorServer connectorServer = JMXConnectorServerFactory
34+
.newJMXConnectorServer(jmxUrl, env, beanServer);
35+
36+
connectorServer.start();
37+
}
38+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
public class CorrectRmiInitialisation {
2+
public void initAndStartRmiServer(int port, String hostname, boolean local) {
3+
MBeanServerForwarder authzProxy = null;
4+
5+
env.put("jmx.remote.x.daemon", "true");
6+
7+
/* Restrict the login function to String Objects only (see CVE-2016-3427) */
8+
Map<String, Object> env = new HashMap<String, Object>();
9+
// For Java 10+
10+
String stringsOnlyFilter = "java.lang.String;!*"; // Deny everything but java.lang.String
11+
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, stringsOnlyFilter);
12+
13+
/* Java 9 or below
14+
env.put("jmx.remote.rmi.server.credential.types",
15+
new String[] { String[].class.getName(), String.class.getName() });
16+
*/
17+
18+
int rmiPort = Integer.getInteger("com.sun.management.jmxremote.rmi.port", 0);
19+
RMIJRMPServerImpl server = new RMIJRMPServerImpl(rmiPort,
20+
(RMIClientSocketFactory) env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE),
21+
(RMIServerSocketFactory) env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE), env);
22+
23+
JMXServiceURL serviceURL = new JMXServiceURL("rmi", hostname, rmiPort);
24+
25+
// Create RMI Server
26+
RMIConnectorServer jmxServer = new RMIConnectorServer(serviceURL, env, server,
27+
ManagementFactory.getPlatformMBeanServer());
28+
29+
jmxServer.start();
30+
31+
}
32+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>For special use cases some applications may implement a custom service which handles JMX-RMI connections.</p>
8+
9+
<p>When creating such a custom service, a developer should pass a certain environment configuration to the JMX-RMI server initalisation,
10+
as otherwise the JMX-RMI service is susceptible to an unsafe deserialization vulnerability.</p>
11+
12+
<p>This is because the JMX-RMI service allows attackers to supply arbitrary objects to the service authentication
13+
method, resulting in the attempted deserialization of an attacker-controlled object.
14+
In the worst case scenario this could allow an attacker to achieve remote code execution within the context of the application server.</p>
15+
16+
<p>By setting the appropriate environment, the deserialization can be controlled via a deserialization filter.</p>
17+
18+
</overview>
19+
20+
<recommendation>
21+
<p>During the creation of a custom JMX-RMI service an environment should be supplied that sets a deserialization filter.
22+
Ideally this filter should be as restrictive as possible, for example to only allow the deserialization of <code>java.lang.String</code>.</p>
23+
24+
<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>).
25+
The filter should (ideally) only allow java.lang.String and disallow all other classes for deserialization: (<code>"java.lang.String;!*"</code>).</p>
26+
27+
<p>The key-value pair can be set as following:</p>
28+
29+
<sample src="example_filter_java_10.java" />
30+
31+
<p>For applications using Java 6u113 to 9:</p>
32+
33+
<sample src="example_filter_java_9.java" />
34+
35+
<p>Please note that the JMX-RMI service is vulnerable in the default configuration.
36+
For this reason an initialization with a <code>null</code> environment is also vulnerable.</p>
37+
</recommendation>
38+
39+
<example>
40+
<p>The following examples show how an JMX-RMI service can be initialized securely.</p>
41+
42+
<p>The first example shows how an JMX server is initialized securely with the <code>JMXConnectorServerFactory.newJMXConnectorServer()</code> call.</p>
43+
44+
<sample src="CorrectJMXConnectorServerFactoryEnvironmentInitialisation.java" />
45+
46+
<p>The second example shows how a JMX Server is initialized securely if the <code>RMIConnectorServer</code> class is used.</p>
47+
48+
<sample src="CorrectRMIConnectorServerEnvironmentInitalisation.java" />
49+
50+
</example>
51+
52+
<references>
53+
<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>
54+
<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>
55+
<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>
56+
<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>
57+
<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>
58+
</references>
59+
60+
</qhelp>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* @name InsecureRmiJmxAuthenticationEnvironment
3+
* @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.
4+
* @kind problem
5+
* @problem.severity error
6+
* @tags security
7+
* external/cwe/cwe-665
8+
* @precision high
9+
* @id java/insecure-rmi-jmx-server-initialization
10+
*/
11+
12+
import java
13+
import semmle.code.java.dataflow.DataFlow
14+
import semmle.code.java.Maps
15+
16+
/** Holds if `constructor` instantiates an RMI or JMX server. */
17+
predicate isRmiOrJmxServerCreateConstructor(Constructor constructor) {
18+
constructor
19+
.getDeclaringType()
20+
.hasQualifiedName("javax.management.remote.rmi", "RMIConnectorServer")
21+
}
22+
23+
/** Holds if `method` creates an RMI or JMX server. */
24+
predicate isRmiOrJmxServerCreateMethod(Method method) {
25+
method.getName() = "newJMXConnectorServer" and
26+
method.getDeclaringType().hasQualifiedName("javax.management.remote", "JMXConnectorServerFactory")
27+
}
28+
29+
/**
30+
* Models flow from the qualifier of a
31+
* `map.put("jmx.remote.rmi.server.credential.types", value)` call
32+
* to an RMI or JMX initialisation call.
33+
*/
34+
class SafeFlow extends DataFlow::Configuration {
35+
SafeFlow() { this = "MapToPutCredentialstypeConfiguration" }
36+
37+
override predicate isSource(DataFlow::Node source) { putsCredentialtypesKey(source.asExpr()) }
38+
39+
override predicate isSink(DataFlow::Node sink) {
40+
exists(Call c |
41+
isRmiOrJmxServerCreateConstructor(c.getCallee()) or
42+
isRmiOrJmxServerCreateMethod(c.getCallee())
43+
|
44+
sink.asExpr() = c.getArgument(1)
45+
)
46+
}
47+
48+
/**
49+
* Holds if a `put` call on `qualifier` puts a key match
50+
* into the map.
51+
*/
52+
private predicate putsCredentialtypesKey(Expr qualifier) {
53+
exists(MapPutCall put |
54+
put.getKey().(CompileTimeConstantExpr).getStringValue() =
55+
[
56+
"jmx.remote.rmi.server.credential.types",
57+
"jmx.remote.rmi.server.credentials.filter.pattern"
58+
]
59+
or
60+
put.getKey()
61+
.(FieldAccess)
62+
.getField()
63+
.hasQualifiedName("javax.management.remote.rmi", "RMIConnectorServer",
64+
["CREDENTIAL_TYPES", "CREDENTIALS_FILTER_PATTERN"])
65+
|
66+
put.getQualifier() = qualifier and
67+
put.getMethod().(MapMethod).getReceiverKeyType() instanceof TypeString and
68+
put.getMethod().(MapMethod).getReceiverValueType() instanceof TypeObject
69+
)
70+
}
71+
}
72+
73+
/** 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 */
74+
string getRmiResult(Expr e) {
75+
// We got a Map so we have a source and a sink node
76+
if e instanceof NullLiteral
77+
then
78+
result =
79+
"RMI/JMX server initialized with a null environment. Missing type restriction in RMI authentication method exposes the application to deserialization attacks."
80+
else
81+
result =
82+
"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."
83+
}
84+
85+
from Call c, Expr envArg
86+
where
87+
(isRmiOrJmxServerCreateConstructor(c.getCallee()) or isRmiOrJmxServerCreateMethod(c.getCallee())) and
88+
envArg = c.getArgument(1) and
89+
not any(SafeFlow conf).hasFlowToExpr(envArg)
90+
select c, getRmiResult(envArg), envArg, envArg.toString()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
String stringsOnlyFilter = "java.lang.String;!*"; // Deny everything but java.lang.String
2+
3+
Map<String, Object> env = new HashMap<String, Object>;
4+
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, stringsOnlyFilter);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// This is deprecated in Java 10+ !
2+
Map<String, Object>; env = new HashMap<String, Object>;
3+
env.put (
4+
"jmx.remote.rmi.server.credential.types",
5+
new String[]{
6+
String[].class.getName(),
7+
String.class.getName()
8+
}
9+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
| 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 |
2+
| 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 |
3+
| 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 |
4+
| 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 |
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import java.io.IOException;
2+
import javax.management.remote.JMXConnectorServerFactory;
3+
import javax.management.remote.rmi.RMIConnectorServer;
4+
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
8+
public class InsecureRmiJmxEnvironmentConfiguration {
9+
10+
public void initInsecureJmxDueToNullEnv() throws IOException {
11+
// Bad initializing env (arg1) with null
12+
JMXConnectorServerFactory.newJMXConnectorServer(null, null, null);
13+
}
14+
15+
public void initInsecureRmiDueToNullEnv() throws IOException {
16+
// Bad initializing env (arg1) with null
17+
new RMIConnectorServer(null, null, null, null);
18+
}
19+
20+
public void initInsecureRmiDueToMissingEnvKeyValue() throws IOException {
21+
// Bad initializing env (arg1) with missing
22+
// "jmx.remote.rmi.server.credential.types"
23+
Map<String, Object> env = new HashMap<>();
24+
env.put("jmx.remote.x.daemon", "true");
25+
new RMIConnectorServer(null, env, null, null);
26+
}
27+
28+
public void initInsecureJmxDueToMissingEnvKeyValue() throws IOException {
29+
// Bad initializing env (arg1) with missing
30+
// "jmx.remote.rmi.server.credential.types"
31+
Map<String, Object> env = new HashMap<>();
32+
env.put("jmx.remote.x.daemon", "true");
33+
JMXConnectorServerFactory.newJMXConnectorServer(null, env, null);
34+
}
35+
36+
public void secureJmxConnnectorServer() throws IOException {
37+
// Good
38+
Map<String, Object> env = new HashMap<>();
39+
env.put("jmx.remote.x.daemon", "true");
40+
env.put("jmx.remote.rmi.server.credential.types",
41+
new String[] { String[].class.getName(), String.class.getName() });
42+
JMXConnectorServerFactory.newJMXConnectorServer(null, env, null);
43+
}
44+
45+
public void secureRmiConnnectorServer() throws IOException {
46+
// Good
47+
Map<String, Object> env = new HashMap<>();
48+
env.put("jmx.remote.x.daemon", "true");
49+
env.put("jmx.remote.rmi.server.credential.types",
50+
new String[] { String[].class.getName(), String.class.getName() });
51+
new RMIConnectorServer(null, env, null, null);
52+
}
53+
54+
public void secureeJmxConnectorServerConstants1() throws IOException {
55+
// Good
56+
Map<String, Object> env = new HashMap<>();
57+
env.put("jmx.remote.x.daemon", "true");
58+
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, "java.lang.String;!*"); // Deny everything but
59+
// java.lang.String
60+
JMXConnectorServerFactory.newJMXConnectorServer(null, env, null);
61+
}
62+
63+
public void secureeRmiConnectorServerConstants1() throws IOException {
64+
// Good
65+
Map<String, Object> env = new HashMap<>();
66+
env.put("jmx.remote.x.daemon", "true");
67+
String stringsOnlyFilter = "java.lang.String;!*"; // Deny everything but java.lang.String
68+
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, stringsOnlyFilter);
69+
new RMIConnectorServer(null, env, null, null);
70+
}
71+
72+
public void secureJmxConnectorServerConstants2() throws IOException {
73+
// Good
74+
Map<String, Object> env = new HashMap<>();
75+
env.put("jmx.remote.x.daemon", "true");
76+
env.put("jmx.remote.rmi.server.credentials.filter.pattern", "java.lang.String;!*"); // Deny everything but
77+
// java.lang.String
78+
JMXConnectorServerFactory.newJMXConnectorServer(null, env, null);
79+
}
80+
81+
public void secureRmiConnectorServerConstants2() throws IOException {
82+
// Good
83+
Map<String, Object> env = new HashMap<>();
84+
env.put("jmx.remote.x.daemon", "true");
85+
String stringsOnlyFilter = "java.lang.String;!*"; // Deny everything but java.lang.String
86+
env.put("jmx.remote.rmi.server.credentials.filter.pattern", stringsOnlyFilter);
87+
new RMIConnectorServer(null, env, null, null);
88+
}
89+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE/CWE-665/InsecureRmiJmxEnvironmentConfiguration.ql
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/rmi-remote-0.0.0

0 commit comments

Comments
 (0)