-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Java: Unsafe RMI deserialization #5818
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
8b96ff9
First draft of RmiUnsafeDeserialization.ql
artem-smotrakov efa4b4f
Cover Registry in RmiUnsafeDeserialization.ql
artem-smotrakov ec6186a
Draft of tests for RmiUnsafeDeserialization.ql
artem-smotrakov 3d20330
More tests for RmiUnsafeDeserialization
artem-smotrakov 5ffe04d
Updated expected output for RmiUnsafeDeserialization.java test
artem-smotrakov 0182dfe
Added RmiUnsafeDeserialization.qhelp
artem-smotrakov e28f919
Look for remote callable method only in RmiUnsafeDeserialization.ql
artem-smotrakov 2d93eea
Covered deserialization filters in RmiUnsafeDeserialization.ql
artem-smotrakov d2e29fc
Renamed RmiUnsafeDeserialization.ql -> UnsafeDeserializationRmi.ql
artem-smotrakov c837605
Added test cases with sanitizers for UnsafeDeserializationRmi.ql
artem-smotrakov 1b51dd4
Added an example with deserialization filter to UnsafeDeserialization…
artem-smotrakov 62c6bee
Simplified UnsafeDeserializationRmi.ql
artem-smotrakov b28d639
Fixed errors in UnsafeDeserializationRmi.qhelp
artem-smotrakov 8dc1451
Better recommendation in UnsafeDeserializationRmi.qhelp
artem-smotrakov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
9 changes: 9 additions & 0 deletions
9
java/ql/src/experimental/Security/CWE/CWE-502/RmiRemoteObjectWithFilter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| public void bindRemoteObject(Registry registry, int port) throws Exception { | ||
| ObjectInputFilter filter = info -> { | ||
| if (info.serialClass().getCanonicalName().startsWith("com.safe.package.")) { | ||
| return ObjectInputFilter.Status.ALLOWED; | ||
| } | ||
| return ObjectInputFilter.Status.REJECTED; | ||
| }; | ||
| registry.bind("safer", UnicastRemoteObject.exportObject(new RemoteObjectImpl(), port, filter)); | ||
| } |
14 changes: 14 additions & 0 deletions
14
java/ql/src/experimental/Security/CWE/CWE-502/RmiSafeRemoteObject.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| public class Server { | ||
| public void bindRemoteObject(Registry registry) throws Exception { | ||
| registry.bind("safe", new RemoteObjectImpl()); | ||
| } | ||
| } | ||
|
|
||
| interface RemoteObject extends Remote { | ||
| void calculate(int a, double b) throws RemoteException; | ||
| void save(String s) throws RemoteException; | ||
| } | ||
|
|
||
| class RemoteObjectImpl implements RemoteObject { | ||
| // ... | ||
| } |
13 changes: 13 additions & 0 deletions
13
java/ql/src/experimental/Security/CWE/CWE-502/RmiUnsafeRemoteObject.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| public class Server { | ||
| public void bindRemoteObject(Registry registry) throws Exception { | ||
| registry.bind("unsafe", new RemoteObjectImpl()); | ||
| } | ||
| } | ||
|
|
||
| interface RemoteObject extends Remote { | ||
| void action(Object obj) throws RemoteException; | ||
| } | ||
|
|
||
| class RemoteObjectImpl implements RemoteObject { | ||
| // ... | ||
| } |
81 changes: 81 additions & 0 deletions
81
java/ql/src/experimental/Security/CWE/CWE-502/UnsafeDeserializationRmi.qhelp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| <!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd"> | ||
| <qhelp> | ||
|
|
||
| <overview> | ||
| <p> | ||
| Java RMI uses the default Java serialization mechanism (in other words, <code>ObjectInputStream</code>) | ||
| to pass parameters in remote method invocations. This mechanism is known to be unsafe when deserializing | ||
| untrusted data. If a registered remote object has a method that accepts a complex object, | ||
| an attacker can take advantage of the unsafe deserialization mechanism. | ||
| In the worst case, it results in remote code execution. | ||
| </p> | ||
| </overview> | ||
|
|
||
| <recommendation> | ||
| <p> | ||
| Use only strings and primitive types for parameters of remotely invokable methods. | ||
| </p> | ||
| <p> | ||
| Set a filter for incoming serialized data by wrapping remote objects using either <code>UnicastRemoteObject.exportObject(Remote, int, ObjectInputFilter)</code> | ||
| or <code>UnicastRemoteObject.exportObject(Remote, int, RMIClientSocketFactory, RMIServerSocketFactory, ObjectInputFilter)</code> methods. | ||
| Those methods accept an <code>ObjectInputFilter</code> that decides which classes are allowed for deserialization. | ||
| The filter should allow deserializing only safe classes. | ||
| </p> | ||
| <p> | ||
| It is also possible to set a process-wide deserialization filter. | ||
| The filter can be set by with <code>ObjectInputFilter.Config.setSerialFilter(ObjectInputFilter)</code> method, | ||
| or by setting system or security property <code>jdk.serialFilter</code>. | ||
| Make sure that you use the latest Java versions that include JEP 290. | ||
| Please note that the query is not sensitive to this mitigation. | ||
| </p> | ||
| <p> | ||
| If switching to the latest Java versions is not possible, | ||
| consider using other implementations of remote procedure calls. For example, HTTP API with JSON. | ||
| Make sure that the underlying deserialization mechanism is properly configured | ||
| so that deserialization attacks are not possible. | ||
| </p> | ||
| </recommendation> | ||
|
|
||
| <example> | ||
| <p> | ||
| The following code registers a remote object | ||
| with a vulnerable method that accepts a complex object: | ||
| </p> | ||
| <sample src="RmiUnsafeRemoteObject.java" /> | ||
|
|
||
| <p> | ||
| The next example registers a safe remote object | ||
| whose methods use only primitive types and strings: | ||
| </p> | ||
| <sample src="RmiSafeRemoteObject.java" /> | ||
|
|
||
| <p> | ||
| The next example shows how to set a deserilization filter for a remote object: | ||
| </p> | ||
| <sample src="RmiRemoteObjectWithFilter.java" /> | ||
|
|
||
| </example> | ||
|
|
||
| <references> | ||
| <li> | ||
| Oracle: | ||
| <a href="https://www.oracle.com/java/technologies/javase/remote-method-invocation-home.html">Remote Method Invocation (RMI)</a>. | ||
| </li> | ||
| <li> | ||
| ITNEXT: | ||
| <a href="https://itnext.io/java-rmi-for-pentesters-part-two-reconnaissance-attack-against-non-jmx-registries-187a6561314d">Java RMI for pentesters part two - reconnaissance & attack against non-JMX registries</a>. | ||
| </li> | ||
| <li> | ||
| MOGWAI LABS: | ||
| <a href="https://mogwailabs.de/en/blog/2019/03/attacking-java-rmi-services-after-jep-290">Attacking Java RMI services after JEP 290</a> | ||
| </li> | ||
| <li> | ||
| OWASP: | ||
| <a href="https://www.owasp.org/index.php/Deserialization_of_untrusted_data">Deserialization of untrusted data</a>. | ||
| </li> | ||
| <li> | ||
| OpenJDK: | ||
| <a href="https://openjdk.java.net/jeps/290">JEP 290: Filter Incoming Serialization Data</a> | ||
| </li> | ||
| </references> | ||
| </qhelp> | ||
78 changes: 78 additions & 0 deletions
78
java/ql/src/experimental/Security/CWE/CWE-502/UnsafeDeserializationRmi.ql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /** | ||
| * @name Unsafe deserialization in a remotely callable method. | ||
| * @description If a registered remote object has a method that accepts a complex object, | ||
| * an attacker can take advantage of the unsafe deserialization mechanism | ||
| * which is used to pass parameters in RMI. | ||
| * In the worst case, it results in remote code execution. | ||
| * @kind path-problem | ||
| * @problem.severity error | ||
| * @precision high | ||
| * @id java/unsafe-deserialization-rmi | ||
| * @tags security | ||
| * external/cwe/cwe-502 | ||
| */ | ||
|
|
||
| import java | ||
| import semmle.code.java.dataflow.TaintTracking | ||
| import semmle.code.java.frameworks.Rmi | ||
| import DataFlow::PathGraph | ||
|
|
||
| /** | ||
| * A method that binds a name to a remote object. | ||
| */ | ||
| private class BindMethod extends Method { | ||
| BindMethod() { | ||
| ( | ||
| getDeclaringType().hasQualifiedName("java.rmi", "Naming") or | ||
| getDeclaringType().hasQualifiedName("java.rmi.registry", "Registry") | ||
| ) and | ||
| hasName(["bind", "rebind"]) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Holds if `type` has an vulnerable remote method. | ||
| */ | ||
| private predicate hasVulnerableMethod(RefType type) { | ||
| exists(RemoteCallableMethod m, Type parameterType | | ||
| m.getDeclaringType() = type and parameterType = m.getAParamType() | ||
| | | ||
| not parameterType instanceof PrimitiveType and | ||
| not parameterType instanceof TypeString and | ||
| not parameterType.(RefType).hasQualifiedName("java.io", "ObjectInputStream") | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * A taint-tracking configuration for unsafe remote objects | ||
| * that are vulnerable to deserialization attacks. | ||
| */ | ||
| private class BindingUnsafeRemoteObjectConfig extends TaintTracking::Configuration { | ||
| BindingUnsafeRemoteObjectConfig() { this = "BindingUnsafeRemoteObjectConfig" } | ||
|
|
||
| override predicate isSource(DataFlow::Node source) { | ||
| exists(ConstructorCall cc | cc = source.asExpr() | | ||
| hasVulnerableMethod(cc.getConstructedType().getASupertype*()) | ||
| ) | ||
| } | ||
|
|
||
| override predicate isSink(DataFlow::Node sink) { | ||
| exists(MethodAccess ma | ma.getArgument(1) = sink.asExpr() | | ||
| ma.getMethod() instanceof BindMethod | ||
| ) | ||
| } | ||
|
|
||
| override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) { | ||
| exists(MethodAccess ma, Method m | m = ma.getMethod() | | ||
| m.getDeclaringType().hasQualifiedName("java.rmi.server", "UnicastRemoteObject") and | ||
| m.hasName("exportObject") and | ||
| not m.getParameterType([2, 4]).(RefType).hasQualifiedName("java.io", "ObjectInputFilter") and | ||
| ma.getArgument(0) = fromNode.asExpr() and | ||
| ma = toNode.asExpr() | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| from DataFlow::PathNode source, DataFlow::PathNode sink, BindingUnsafeRemoteObjectConfig conf | ||
| where conf.hasFlowPath(source, sink) | ||
| select sink.getNode(), source, sink, "Unsafe deserialization in a remote object." |
15 changes: 15 additions & 0 deletions
15
java/ql/test/experimental/query-tests/security/CWE-502/UnsafeDeserializationRmi.expected
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| edges | ||
| | UnsafeDeserializationRmi.java:17:68:17:95 | new UnsafeRemoteObjectImpl(...) : UnsafeRemoteObjectImpl | UnsafeDeserializationRmi.java:17:35:17:96 | exportObject(...) | | ||
| nodes | ||
| | UnsafeDeserializationRmi.java:15:33:15:60 | new UnsafeRemoteObjectImpl(...) | semmle.label | new UnsafeRemoteObjectImpl(...) | | ||
| | UnsafeDeserializationRmi.java:16:35:16:62 | new UnsafeRemoteObjectImpl(...) | semmle.label | new UnsafeRemoteObjectImpl(...) | | ||
| | UnsafeDeserializationRmi.java:17:35:17:96 | exportObject(...) | semmle.label | exportObject(...) | | ||
| | UnsafeDeserializationRmi.java:17:68:17:95 | new UnsafeRemoteObjectImpl(...) : UnsafeRemoteObjectImpl | semmle.label | new UnsafeRemoteObjectImpl(...) : UnsafeRemoteObjectImpl | | ||
| | UnsafeDeserializationRmi.java:29:31:29:58 | new UnsafeRemoteObjectImpl(...) | semmle.label | new UnsafeRemoteObjectImpl(...) | | ||
| | UnsafeDeserializationRmi.java:30:33:30:60 | new UnsafeRemoteObjectImpl(...) | semmle.label | new UnsafeRemoteObjectImpl(...) | | ||
| #select | ||
| | UnsafeDeserializationRmi.java:15:33:15:60 | new UnsafeRemoteObjectImpl(...) | UnsafeDeserializationRmi.java:15:33:15:60 | new UnsafeRemoteObjectImpl(...) | UnsafeDeserializationRmi.java:15:33:15:60 | new UnsafeRemoteObjectImpl(...) | Unsafe deserialization in a remote object. | | ||
| | UnsafeDeserializationRmi.java:16:35:16:62 | new UnsafeRemoteObjectImpl(...) | UnsafeDeserializationRmi.java:16:35:16:62 | new UnsafeRemoteObjectImpl(...) | UnsafeDeserializationRmi.java:16:35:16:62 | new UnsafeRemoteObjectImpl(...) | Unsafe deserialization in a remote object. | | ||
| | UnsafeDeserializationRmi.java:17:35:17:96 | exportObject(...) | UnsafeDeserializationRmi.java:17:68:17:95 | new UnsafeRemoteObjectImpl(...) : UnsafeRemoteObjectImpl | UnsafeDeserializationRmi.java:17:35:17:96 | exportObject(...) | Unsafe deserialization in a remote object. | | ||
| | UnsafeDeserializationRmi.java:29:31:29:58 | new UnsafeRemoteObjectImpl(...) | UnsafeDeserializationRmi.java:29:31:29:58 | new UnsafeRemoteObjectImpl(...) | UnsafeDeserializationRmi.java:29:31:29:58 | new UnsafeRemoteObjectImpl(...) | Unsafe deserialization in a remote object. | | ||
| | UnsafeDeserializationRmi.java:30:33:30:60 | new UnsafeRemoteObjectImpl(...) | UnsafeDeserializationRmi.java:30:33:30:60 | new UnsafeRemoteObjectImpl(...) | UnsafeDeserializationRmi.java:30:33:30:60 | new UnsafeRemoteObjectImpl(...) | Unsafe deserialization in a remote object. | |
73 changes: 73 additions & 0 deletions
73
java/ql/test/experimental/query-tests/security/CWE-502/UnsafeDeserializationRmi.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import java.io.ObjectInputFilter; | ||
| import java.io.ObjectInputStream; | ||
| import java.rmi.Naming; | ||
| import java.rmi.Remote; | ||
| import java.rmi.RemoteException; | ||
| import java.rmi.registry.LocateRegistry; | ||
| import java.rmi.registry.Registry; | ||
| import java.rmi.server.UnicastRemoteObject; | ||
|
|
||
| public class UnsafeDeserializationRmi { | ||
|
|
||
| // BAD (bind a remote object that has a vulnerable method) | ||
| public static void testRegistryBindWithObjectParameter() throws Exception { | ||
| Registry registry = LocateRegistry.createRegistry(1099); | ||
| registry.bind("unsafe", new UnsafeRemoteObjectImpl()); | ||
| registry.rebind("unsafe", new UnsafeRemoteObjectImpl()); | ||
| registry.rebind("unsafe", UnicastRemoteObject.exportObject(new UnsafeRemoteObjectImpl())); | ||
| } | ||
|
|
||
| // GOOD (bind a remote object that has methods that takes safe parameters) | ||
| public static void testRegistryBindWithIntParameter() throws Exception { | ||
| Registry registry = LocateRegistry.createRegistry(1099); | ||
| registry.bind("safe", new SafeRemoteObjectImpl()); | ||
| registry.rebind("safe", new SafeRemoteObjectImpl()); | ||
| } | ||
|
|
||
| // BAD (bind a remote object that has a vulnerable method) | ||
| public static void testNamingBindWithObjectParameter() throws Exception { | ||
| Naming.bind("unsafe", new UnsafeRemoteObjectImpl()); | ||
| Naming.rebind("unsafe", new UnsafeRemoteObjectImpl()); | ||
| } | ||
|
|
||
| // GOOD (bind a remote object that has methods that takes safe parameters) | ||
| public static void testNamingBindWithIntParameter() throws Exception { | ||
| Naming.bind("safe", new SafeRemoteObjectImpl()); | ||
| Naming.rebind("safe", new SafeRemoteObjectImpl()); | ||
| } | ||
|
|
||
| // GOOD (bind a remote object with a deserialization filter) | ||
| public static void testRegistryBindWithDeserializationFilter() throws Exception { | ||
| Registry registry = LocateRegistry.createRegistry(1099); | ||
| ObjectInputFilter filter = info -> { | ||
| if (info.serialClass().getCanonicalName().startsWith("com.safe.package.")) { | ||
| return ObjectInputFilter.Status.ALLOWED; | ||
| } | ||
| return ObjectInputFilter.Status.REJECTED; | ||
| }; | ||
| registry.rebind("safe", UnicastRemoteObject.exportObject(new UnsafeRemoteObjectImpl(), 12345, filter)); | ||
| } | ||
| } | ||
|
|
||
| interface UnsafeRemoteObject extends Remote { | ||
| void take(Object obj) throws RemoteException; | ||
| } | ||
|
|
||
| class UnsafeRemoteObjectImpl implements UnsafeRemoteObject { | ||
| public void take(Object obj) throws RemoteException {} | ||
| } | ||
|
|
||
| interface SafeRemoteObject extends Remote { | ||
| void take(int n) throws RemoteException; | ||
| void take(double n) throws RemoteException; | ||
| void take(String s) throws RemoteException; | ||
| void take(ObjectInputStream ois) throws RemoteException; | ||
| } | ||
|
|
||
| class SafeRemoteObjectImpl implements SafeRemoteObject { | ||
| public void take(int n) throws RemoteException {} | ||
| public void take(double n) throws RemoteException {} | ||
| public void take(String s) throws RemoteException {} | ||
| public void take(ObjectInputStream ois) throws RemoteException {} | ||
| public void safeMethod(Object object) {} // this method is not declared in SafeRemoteObject | ||
| } |
1 change: 1 addition & 0 deletions
1
java/ql/test/experimental/query-tests/security/CWE-502/UnsafeDeserializationRmi.qlref
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| experimental/Security/CWE/CWE-502/UnsafeDeserializationRmi.ql |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this query doesn't check for either of these mitigations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, it doesn't. I thought about covering these mitigations in the query. To prevent deserialization attacks here,
ObjectInputFilter.Config.setSerialFilter()should be called before actual RMI calls happen. I am not sure whether this check can be implemented with CodeQL or not. It is even more complicated with thejdk.serialFilterproperty. The property has to be set before the classes for deserialization mechanism (ObjectInputStreamand others) are loaded because the property is read only once when the mechanism is accessed for the first time.I'd prefer to keep these mitigations here because they are still valid if used correctly.
Please let me know if you have an idea how to reliably check there mitigations in the query.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just noting in the qhelp that these are valid mitigations but the query isn't sensitive to them is enough