Skip to content

Commit 15a3068

Browse files
author
Timo Mueller
committed
Added query for insecure environment configuration RMI JMX (CVE-2016-8735)
1 parent ecd40e5 commit 15a3068

7 files changed

+375
-0
lines changed
Lines changed: 38 additions & 0 deletions
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 my_filter = "java.lang.String;!*"; // Deny everything but java.lang.String
21+
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, my_filter);
22+
23+
/* Old way
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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
public class CorrectRmiInitialisation {
2+
public void initAndStartRmiServer(int port, String hostname, boolean local) {
3+
Map<String, Object> env = new HashMap<>();
4+
5+
MBeanServerForwarder authzProxy = null;
6+
7+
env.put("jmx.remote.x.daemon", "true");
8+
9+
/* Restrict the login function to String Objects only (see CVE-2016-3427) */
10+
Map<String, Object> env = new HashMap<String, Object>();
11+
// For Java 10+
12+
String my_filter = "java.lang.String;!*"; // Deny everything but java.lang.String
13+
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, my_filter);
14+
15+
/* Old way
16+
env.put("jmx.remote.rmi.server.credential.types",
17+
new String[] { String[].class.getName(), String.class.getName() });
18+
*/
19+
20+
int rmiPort = Integer.getInteger("com.sun.management.jmxremote.rmi.port", 0);
21+
RMIJRMPServerImpl server = new RMIJRMPServerImpl(rmiPort,
22+
(RMIClientSocketFactory) env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE),
23+
(RMIServerSocketFactory) env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE), env);
24+
25+
JMXServiceURL serviceURL = new JMXServiceURL("rmi", hostname, rmiPort);
26+
27+
// Create RMI Server
28+
RMIConnectorServer jmxServer = new RMIConnectorServer(serviceURL, env, server,
29+
ManagementFactory.getPlatformMBeanServer());
30+
31+
jmxServer.start();
32+
33+
}
34+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>An improperly set environment variable during the creation of an RMI or JMX server can lead
7+
to an unauthenticated remote code execution vulnerability. This is due to the fact that the
8+
RMI/JMX server environment allows attackers to supply arbitrary objects to the authentication
9+
method, resulting in the attempted deserialization of an attacker-controlled object.
10+
</overview>
11+
12+
<recommendation>
13+
<p>During the creation/initialitation of an RMI or JMX server a properly set environment (Map) variable has
14+
to be passed as second parameter.
15+
In order to disallow the deserialization of arbitrary objects the passed environment needs to set a deserialization filter.
16+
Ideally this filter only allows the deserialization to <code>java.lang.String</code>.
17+
18+
The filter can be configured by setting the key <code>jmx.remote.rmi.server.credentials.filter.pattern</code> (CONST variable <code>RMIConnectorServer.CREDENTIALS_FILTER_PATTERN</code>).
19+
The filter should (ideally) blacklist all classes, and only whitelist java.lang.String for deserialization: (<code> "java.lang.String;!*"</code>).
20+
21+
The key-value pair can be set as following:
22+
23+
<code>
24+
String my_filter = "java.lang.String;!*"; // Deny everything but java.lang.String
25+
26+
Map<String, Object> env = new HashMap<String, Object>;
27+
env.put(RMIConnectorServer.CREDENTIALS_FILTER_PATTERN, my_filter);
28+
</code>
29+
30+
For applications using &lt; Java 10:
31+
32+
<code>
33+
// This is deprecated in Java 10+ !
34+
Map<String, Object> env = new HashMap<String, Object>;
35+
env.put (
36+
"jmx.remote.rmi.server.credential.types",
37+
new String[]{
38+
String[].class.getName(),
39+
String.class.getName()
40+
}
41+
);
42+
</code>
43+
44+
Please note that the authentication implementation is vulnerable by default.
45+
For this reason an initialitation with a <code>null</code> environment is also vulnerable .
46+
</recommendation>
47+
48+
<example>
49+
<p>The following examples show how an RMI or JMX server can be initialized securely.
50+
51+
<p>The first example shows how an RMI server can be initialized with a secure environment.</p>
52+
53+
<sample src="CorrectRmiInitialisation.java">
54+
55+
<p>The second example shows how the environment for a JMX server can be initialized securely.</p>
56+
57+
<sample src="CorrectJmxInitialisation.java">
58+
59+
</example>
60+
61+
<references>
62+
<li>OWASP: <a href="https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data">OWASP Deserialization of untrusted data</a>.</li>
63+
<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>
64+
<li>Vulnerable implementation of the RMI "newClient()" function: <a href="https://docs.oracle.com/javase/8/docs/api/javax/management/remote/rmi/RMIServer.html#newClient-java.lang.Object-">Vulnerable Function</a>.</li>
65+
<li>Oracle release notes fixing the issue: <a href="https://www.oracle.com/java/technologies/javase/8u91-relnotes.html">Rlease Notes</a>.</li>
66+
<li>Documentation for <a href="https://docs.oracle.com/javase/10/docs/api/javax/management/remote/rmi/RMIConnectorServer.html#CREDENTIALS_FILTER_PATTERN">CREDENTIALS_FILTER_PATTERN</a></li>
67+
</references>
68+
</qhelp>
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* @name InsecureRmiJmxAuthenticationEnvironment
3+
* @kind path-problem
4+
* @problem.severity error
5+
* @tags security
6+
* external/cwe/cwe-665
7+
* @precision high
8+
* @id java/insecure-rmi-jmx-server-initalisation
9+
*/
10+
11+
import java
12+
import semmle.code.java.dataflow.DataFlow
13+
import semmle.code.java.dataflow.DataFlow2
14+
import semmle.code.java.Maps
15+
import DataFlow::PathGraph
16+
import semmle.code.java.dataflow.NullGuards
17+
import semmle.code.java.dataflow.Nullness
18+
19+
/** predicate which detects vulnerable Constructors */
20+
predicate isRmiOrJmxServerCreateConstructor(Constructor constructor) {
21+
constructor.getName() = "RMIConnectorServer" and
22+
constructor
23+
.getDeclaringType()
24+
.hasQualifiedName("javax.management.remote.rmi", "RMIConnectorServer")
25+
}
26+
27+
/** Predicate which detects vulnerable server creations via methods */
28+
predicate isRmiOrJmxServerCreateMethod(Method method) {
29+
method.getName() = "newJMXConnectorServer" and
30+
method.getDeclaringType().hasQualifiedName("javax.management.remote", "JMXConnectorServerFactory")
31+
}
32+
33+
/**
34+
* Models flow from `new HashMap<>()` to a
35+
* `map.put("jmx.remote.rmi.server.credential.types", value)` call.
36+
*/
37+
class MapToPutCredentialstypeConfiguration extends DataFlow2::Configuration {
38+
MapToPutCredentialstypeConfiguration() { this = "MapToPutCredentialstypeConfiguration" }
39+
40+
override predicate isSource(DataFlow::Node source) {
41+
source.asExpr().(ClassInstanceExpr).getConstructedType() instanceof MapType
42+
}
43+
44+
override predicate isSink(DataFlow2::Node sink) { putsCredentialtypesKey(sink.asExpr()) }
45+
46+
/**
47+
* Holds if a `put` call on `qualifier` puts a key match
48+
* into the map.
49+
*/
50+
private predicate putsCredentialtypesKey(Expr qualifier) {
51+
exists(MapPutCall put |
52+
put.getKey().(CompileTimeConstantExpr).getStringValue() =
53+
"jmx.remote.rmi.server.credential.types" or
54+
put.getKey().(CompileTimeConstantExpr).getStringValue() =
55+
"jmx.remote.rmi.server.credentials.filter.pattern" or
56+
put.getKey().toString() = "RMIConnectorServer.CREDENTIAL_TYPES" or // This can probably be solved more nicely
57+
put.getKey().toString() = "RMIConnectorServer.CREDENTIALS_FILTER_PATTERN" // This can probably be solved more nicely
58+
|
59+
put.getQualifier() = qualifier and
60+
put.getMethod().(MapMethod).getReceiverKeyType().getName() = "String" and
61+
put.getMethod().(MapMethod).getReceiverValueType().getName() = "Object"
62+
)
63+
}
64+
}
65+
66+
/** Models flow from `new HashMap<>()` to the argument of a `TestConstructor` call. */
67+
class MapToRmiServerInitConfiguration extends DataFlow::Configuration {
68+
MapToRmiServerInitConfiguration() { this = "MapToRmiServerInitConfiguration" }
69+
70+
override predicate isSource(DataFlow::Node source) {
71+
source.asExpr().(ClassInstanceExpr).getConstructedType() instanceof MapType
72+
}
73+
74+
override predicate isSink(DataFlow::Node sink) {
75+
exists(ConstructorCall ccall |
76+
sink.asExpr() = ccall.getArgument(1) and
77+
isRmiOrJmxServerCreateConstructor(ccall.getConstructor())
78+
)
79+
or
80+
exists(MethodAccess ma |
81+
sink.asExpr() = ma.getArgument(1) and
82+
isRmiOrJmxServerCreateMethod(ma.getMethod())
83+
)
84+
}
85+
}
86+
87+
/** Models if any JMX/RMI server are initialized with a null environment */
88+
class FlowServerInitializedWithNullEnv extends DataFlow::Configuration {
89+
FlowServerInitializedWithNullEnv() { this = "FlowServerInitializedWithNullEnv" }
90+
91+
override predicate isSource(DataFlow::Node source) { any() }
92+
93+
override predicate isSink(DataFlow::Node sink) {
94+
exists(ConstructorCall ccall |
95+
sink.asExpr() = ccall and
96+
isRmiOrJmxServerCreateConstructor(ccall.getConstructor()) and
97+
ccall.getArgument(1) = alwaysNullExpr()
98+
)
99+
or
100+
exists(MethodAccess ma |
101+
sink.asExpr() = ma and
102+
isRmiOrJmxServerCreateMethod(ma.getMethod()) and
103+
ma.getArgument(1) = alwaysNullExpr()
104+
)
105+
}
106+
}
107+
108+
/** Returns true if within the passed PathNode a "jmx.remote.rmi.server.credential.types" is set. */
109+
predicate mapFlowContainsCredentialtype(DataFlow::PathNode source) {
110+
exists(MapToPutCredentialstypeConfiguration conf | conf.hasFlow(source.getNode(), _))
111+
}
112+
113+
/** Returns result depending if the vulnerability is present due to a) a null environment b) an insecurely set environment map */
114+
bindingset[source]
115+
string getRmiResult(DataFlow::PathNode source) {
116+
// We got a Map so we have a source and a sink node
117+
if source.getNode().getType() instanceof MapType
118+
then
119+
result =
120+
"RMI/JMX server initialized with insecure environment $@. The $@ never restricts accepted client objects to 'java.lang.String'. This exposes to deserialization attacks against the RMI authentication method."
121+
else
122+
// The environment is not a map so we most likely have a "null" environment and therefore only a sink
123+
result =
124+
"RMI/JMX server initialized with 'null' environment $@. Missing type restriction in RMI authentication method exposes the application to deserialization attacks."
125+
}
126+
127+
/** Predicate returns true for any map flow paths with NO jmx.remote.rmi.server.credential.types set */
128+
predicate hasVulnerableMapFlow(DataFlow::PathNode source, DataFlow::PathNode sink) {
129+
exists(MapToRmiServerInitConfiguration dataflow |
130+
dataflow.hasFlowPath(source, sink) and
131+
not mapFlowContainsCredentialtype(source)
132+
)
133+
}
134+
135+
from
136+
DataFlow::PathNode source, DataFlow::PathNode sink,
137+
FlowServerInitializedWithNullEnv initNullDataflow
138+
where
139+
// Check if server is created with null env
140+
initNullDataflow.hasFlowPath(source, sink)
141+
or
142+
// The map created by `new HashMap<String, Object>()` has to a) flow to the sink and b) there must not exist a (different) sink that would put `"jmx.remote.rmi.server.credential.types"` into `source`. */
143+
hasVulnerableMapFlow(source, sink)
144+
select sink.getNode(), source, sink, getRmiResult(source), sink.getNode(), "here", source.getNode(),
145+
"source environment 'Map'"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
edges
2+
| InsecureRmiServerInitialisation.java:32:31:32:45 | new HashMap<String,Object>(...) : HashMap | InsecureRmiServerInitialisation.java:34:59:34:61 | env |
3+
| InsecureRmiServerInitialisation.java:39:31:39:45 | new HashMap<String,Object>(...) : HashMap | InsecureRmiServerInitialisation.java:43:59:43:61 | env |
4+
| InsecureRmiServerInitialisation.java:57:31:57:45 | new HashMap<String,Object>(...) : HashMap | InsecureRmiServerInitialisation.java:61:59:61:61 | env |
5+
nodes
6+
| InsecureRmiServerInitialisation.java:13:5:13:69 | newJMXConnectorServer(...) | semmle.label | newJMXConnectorServer(...) |
7+
| InsecureRmiServerInitialisation.java:32:31:32:45 | new HashMap<String,Object>(...) : HashMap | semmle.label | new HashMap<String,Object>(...) : HashMap |
8+
| InsecureRmiServerInitialisation.java:34:59:34:61 | env | semmle.label | env |
9+
| InsecureRmiServerInitialisation.java:39:31:39:45 | new HashMap<String,Object>(...) : HashMap | semmle.label | new HashMap<String,Object>(...) : HashMap |
10+
| InsecureRmiServerInitialisation.java:43:59:43:61 | env | semmle.label | env |
11+
| InsecureRmiServerInitialisation.java:57:31:57:45 | new HashMap<String,Object>(...) : HashMap | semmle.label | new HashMap<String,Object>(...) : HashMap |
12+
| InsecureRmiServerInitialisation.java:61:59:61:61 | env | semmle.label | env |
13+
#select
14+
| InsecureRmiServerInitialisation.java:13:5:13:69 | newJMXConnectorServer(...) | InsecureRmiServerInitialisation.java:13:5:13:69 | newJMXConnectorServer(...) | InsecureRmiServerInitialisation.java:13:5:13:69 | newJMXConnectorServer(...) | RMI/JMX server initialized with 'null' environment $@. Missing type restriction in RMI authentication method exposes the application to deserialization attacks. | InsecureRmiServerInitialisation.java:13:5:13:69 | newJMXConnectorServer(...) | here | InsecureRmiServerInitialisation.java:13:5:13:69 | newJMXConnectorServer(...) | source environment 'Map' |
15+
| InsecureRmiServerInitialisation.java:34:59:34:61 | env | InsecureRmiServerInitialisation.java:32:31:32:45 | new HashMap<String,Object>(...) : HashMap | InsecureRmiServerInitialisation.java:34:59:34:61 | env | RMI/JMX server initialized with insecure environment $@. The $@ never restricts accepted client objects to 'java.lang.String'. This exposes to deserialization attacks against the RMI authentication method. | InsecureRmiServerInitialisation.java:34:59:34:61 | env | here | InsecureRmiServerInitialisation.java:32:31:32:45 | new HashMap<String,Object>(...) | source environment 'Map' |
16+
| InsecureRmiServerInitialisation.java:61:59:61:61 | env | InsecureRmiServerInitialisation.java:57:31:57:45 | new HashMap<String,Object>(...) : HashMap | InsecureRmiServerInitialisation.java:61:59:61:61 | env | RMI/JMX server initialized with insecure environment $@. The $@ never restricts accepted client objects to 'java.lang.String'. This exposes to deserialization attacks against the RMI authentication method. | InsecureRmiServerInitialisation.java:61:59:61:61 | env | here | InsecureRmiServerInitialisation.java:57:31:57:45 | new HashMap<String,Object>(...) | source environment 'Map' |
17+
18+
TODO RMI Server is missing due to import errors (See test java file)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import java.io.IOException;
2+
import javax.management.remote.JMXConnectorServerFactory;
3+
4+
// import javax.management.remote.rmi.RMIConnectorServer; Importing this throws an error, therefore we can't test this
5+
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
public class InsecureRmiServerInitialisation {
10+
11+
public void initInsecureJmxDueToNullEnv() throws IOException {
12+
// Bad initializing env (arg1) with null
13+
JMXConnectorServerFactory.newJMXConnectorServer(null, null, null);
14+
}
15+
16+
public void initInsecureRmiDueToNullEnv() throws IOException {
17+
// Bad initializing env (arg1) with null
18+
// new RMIConnectorServer(null, null, null, null); Importing this throws an error, therefore we can't test this
19+
}
20+
21+
public void initInsecureRmiDueToMissingEnvKeyValue() throws IOException {
22+
// Bad initializing env (arg1) with missing
23+
// "jmx.remote.rmi.server.credential.types"
24+
Map<String, Object> env = new HashMap<>();
25+
env.put("jmx.remote.x.daemon", "true");
26+
// new RMIConnectorServer(null, env, null, null); Importing this throws an error, therefore we can't test this
27+
}
28+
29+
public void initInsecureJmxDueToMissingEnvKeyValue() throws IOException {
30+
// Bad initializing env (arg1) with missing
31+
// "jmx.remote.rmi.server.credential.types"
32+
Map<String, Object> env = new HashMap<>();
33+
env.put("jmx.remote.x.daemon", "true");
34+
JMXConnectorServerFactory.newJMXConnectorServer(null, env, null);
35+
}
36+
37+
public void secureJmxConnnectorServer() throws IOException {
38+
// Good
39+
Map<String, Object> env = new HashMap<>();
40+
env.put("jmx.remote.x.daemon", "true");
41+
env.put("jmx.remote.rmi.server.credential.types",
42+
new String[] { String[].class.getName(), String.class.getName() });
43+
JMXConnectorServerFactory.newJMXConnectorServer(null, env, null);
44+
}
45+
46+
public void secureRmiConnnectorServer() throws IOException {
47+
// Good
48+
Map<String, Object> env = new HashMap<>();
49+
env.put("jmx.remote.x.daemon", "true");
50+
env.put("jmx.remote.rmi.server.credential.types",
51+
new String[] { String[].class.getName(), String.class.getName() });
52+
// new RMIConnectorServer(null, env, null, null); Importing this throws an error, therefore we can't test this
53+
}
54+
55+
public void secureeJmxConnectorServerConstants() throws IOException {
56+
// Good
57+
Map<String, Object> env = new HashMap<>();
58+
env.put("jmx.remote.x.daemon", "true");
59+
env.put("RMIConnectorServer.SERIAL_FILTER_PATTERN",
60+
new String[] { String[].class.getName(), String.class.getName() });
61+
JMXConnectorServerFactory.newJMXConnectorServer(null, env, null);
62+
}
63+
public void secureeRmiConnectorServerConstants() throws IOException {
64+
// Good
65+
Map<String, Object> env = new HashMap<>();
66+
env.put("jmx.remote.x.daemon", "true");
67+
env.put("RMIConnectorServer.SERIAL_FILTER_PATTERN",
68+
new String[] { String[].class.getName(), String.class.getName() });
69+
// new RMIConnectorServer(null, env, null, null); Importing this throws an error, therefore we can't test this
70+
}
71+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE/CWE-665/InsecureRmiServerInitialisation.ql

0 commit comments

Comments
 (0)