Merge branch 'main' into atorralba/promote-mvel-injection

This commit is contained in:
Tony Torralba
2021-06-16 15:44:43 +02:00
committed by GitHub
800 changed files with 15540 additions and 6277 deletions

View File

@@ -1,63 +0,0 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
Java EXpression Language (JEXL) is a simple expression language
provided by the Apache Commons JEXL library.
The syntax is close to a mix of ECMAScript and shell-script.
The language allows invocation of methods available in the JVM.
If a JEXL expression is built using attacker-controlled data,
and then evaluated, then it may allow the attacker to run arbitrary code.
</p>
</overview>
<recommendation>
<p>
It is generally recommended to avoid using untrusted input in a JEXL expression.
If it is not possible, JEXL expressions should be run in a sandbox that allows accessing only
explicitly allowed classes.
</p>
</recommendation>
<example>
<p>
The following example uses untrusted data to build and run a JEXL expression.
</p>
<sample src="UnsafeJexlExpressionEvaluation.java" />
<p>
The next example shows how an untrusted JEXL expression can be run
in a sandbox that allows accessing only methods in the <code>java.lang.Math</code> class.
The sandbox is implemented using <code>JexlSandbox</code> class that is provided by
Apache Commons JEXL 3.
</p>
<sample src="SaferJexlExpressionEvaluationWithSandbox.java" />
<p>
The next example shows another way how a sandbox can be implemented.
It uses a custom implementation of <code>JexlUberspect</code>
that checks if callees are instances of allowed classes.
</p>
<sample src="SaferJexlExpressionEvaluationWithUberspectSandbox.java" />
</example>
<references>
<li>
Apache Commons JEXL:
<a href="https://commons.apache.org/proper/commons-jexl/">Project page</a>.
</li>
<li>
Apache Commons JEXL documentation:
<a href="https://commons.apache.org/proper/commons-jexl/javadocs/apidocs-2.1.1/">JEXL 2.1.1 API</a>.
</li>
<li>
Apache Commons JEXL documentation:
<a href="https://commons.apache.org/proper/commons-jexl/apidocs/index.html">JEXL 3.1 API</a>.
</li>
<li>
OWASP:
<a href="https://owasp.org/www-community/vulnerabilities/Expression_Language_Injection">Expression Language Injection</a>.
</li>
</references>
</qhelp>

View File

@@ -1,19 +0,0 @@
/**
* @name Expression language injection (JEXL)
* @description Evaluation of a user-controlled JEXL expression
* may lead to arbitrary code execution.
* @kind path-problem
* @problem.severity error
* @precision high
* @id java/jexl-expression-injection
* @tags security
* external/cwe/cwe-094
*/
import java
import JexlInjectionLib
import DataFlow::PathGraph
from DataFlow::PathNode source, DataFlow::PathNode sink, JexlInjectionConfig conf
where conf.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "JEXL injection from $@.", source.getNode(), "this user input"

View File

@@ -1,277 +0,0 @@
import java
import FlowUtils
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
/**
* A taint-tracking configuration for unsafe user input
* that is used to construct and evaluate a JEXL expression.
* It supports both JEXL 2 and 3.
*/
class JexlInjectionConfig extends TaintTracking::Configuration {
JexlInjectionConfig() { this = "JexlInjectionConfig" }
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof JexlEvaluationSink }
override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
any(TaintPropagatingJexlMethodCall c).taintFlow(fromNode, toNode) or
hasGetterFlow(fromNode, toNode)
}
}
/**
* A sink for Expresssion Language injection vulnerabilities via Jexl,
* i.e. method calls that run evaluation of a JEXL expression.
*
* Creating a `Callable` from a tainted JEXL expression or script is considered as a sink
* although the tainted expression is not executed at this point.
* Here we assume that it will get executed at some point,
* maybe stored in an object field and then reached by a different flow.
*/
private class JexlEvaluationSink extends DataFlow::ExprNode {
JexlEvaluationSink() {
exists(MethodAccess ma, Method m, Expr taintFrom |
ma.getMethod() = m and taintFrom = this.asExpr()
|
m instanceof DirectJexlEvaluationMethod and ma.getQualifier() = taintFrom
or
m instanceof CreateJexlCallableMethod and ma.getQualifier() = taintFrom
or
m instanceof JexlEngineGetSetPropertyMethod and
taintFrom.getType() instanceof TypeString and
ma.getAnArgument() = taintFrom
)
}
}
/**
* Defines method calls that propagate tainted data via one of the methods
* from JEXL library.
*/
private class TaintPropagatingJexlMethodCall extends MethodAccess {
Expr taintFromExpr;
TaintPropagatingJexlMethodCall() {
exists(Method m, RefType taintType |
this.getMethod() = m and
taintType = taintFromExpr.getType()
|
isUnsafeEngine(this.getQualifier()) and
(
m instanceof CreateJexlScriptMethod and
taintFromExpr = this.getArgument(0) and
taintType instanceof TypeString
or
m instanceof CreateJexlExpressionMethod and
taintFromExpr = this.getAnArgument() and
taintType instanceof TypeString
or
m instanceof CreateJexlTemplateMethod and
(taintType instanceof TypeString or taintType instanceof Reader) and
taintFromExpr = this.getArgument([0, 1])
)
)
}
/**
* Holds if `fromNode` to `toNode` is a dataflow step that propagates
* tainted data.
*/
predicate taintFlow(DataFlow::Node fromNode, DataFlow::Node toNode) {
fromNode.asExpr() = taintFromExpr and toNode.asExpr() = this
}
}
/**
* Holds if `expr` is a JEXL engine that is not configured with a sandbox.
*/
private predicate isUnsafeEngine(Expr expr) {
not exists(SandboxedJexlFlowConfig config | config.hasFlowTo(DataFlow::exprNode(expr)))
}
/**
* A configuration for a tracking sandboxed JEXL engines.
*/
private class SandboxedJexlFlowConfig extends DataFlow2::Configuration {
SandboxedJexlFlowConfig() { this = "JexlInjection::SandboxedJexlFlowConfig" }
override predicate isSource(DataFlow::Node node) { node instanceof SandboxedJexlSource }
override predicate isSink(DataFlow::Node node) {
exists(MethodAccess ma, Method m | ma.getMethod() = m |
(
m instanceof CreateJexlScriptMethod or
m instanceof CreateJexlExpressionMethod or
m instanceof CreateJexlTemplateMethod
) and
ma.getQualifier() = node.asExpr()
)
}
override predicate isAdditionalFlowStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
createsJexlEngine(fromNode, toNode)
}
}
/**
* Defines a data flow source for JEXL engines configured with a sandbox.
*/
private class SandboxedJexlSource extends DataFlow::ExprNode {
SandboxedJexlSource() {
exists(MethodAccess ma, Method m | m = ma.getMethod() |
m.getDeclaringType() instanceof JexlBuilder and
m.hasName(["uberspect", "sandbox"]) and
m.getReturnType() instanceof JexlBuilder and
this.asExpr() = [ma, ma.getQualifier()]
)
or
exists(ConstructorCall cc |
cc.getConstructedType() instanceof JexlEngine and
cc.getArgument(0).getType() instanceof JexlUberspect and
cc = this.asExpr()
)
}
}
/**
* Holds if `fromNode` to `toNode` is a dataflow step that creates one of the JEXL engines.
*/
private predicate createsJexlEngine(DataFlow::Node fromNode, DataFlow::Node toNode) {
exists(MethodAccess ma, Method m | m = ma.getMethod() |
(m.getDeclaringType() instanceof JexlBuilder or m.getDeclaringType() instanceof JexlEngine) and
m.hasName(["create", "createJxltEngine"]) and
ma.getQualifier() = fromNode.asExpr() and
ma = toNode.asExpr()
)
or
exists(ConstructorCall cc |
cc.getConstructedType() instanceof UnifiedJexl and
cc.getArgument(0) = fromNode.asExpr() and
cc = toNode.asExpr()
)
}
/**
* A methods in the `JexlEngine` class that gets or sets a property with a JEXL expression.
*/
private class JexlEngineGetSetPropertyMethod extends Method {
JexlEngineGetSetPropertyMethod() {
getDeclaringType() instanceof JexlEngine and
hasName(["getProperty", "setProperty"])
}
}
/**
* A method that triggers direct evaluation of JEXL expressions.
*/
private class DirectJexlEvaluationMethod extends Method {
DirectJexlEvaluationMethod() {
getDeclaringType() instanceof JexlExpression and hasName("evaluate")
or
getDeclaringType() instanceof JexlScript and hasName("execute")
or
getDeclaringType() instanceof JxltEngineExpression and hasName(["evaluate", "prepare"])
or
getDeclaringType() instanceof JxltEngineTemplate and hasName("evaluate")
or
getDeclaringType() instanceof UnifiedJexlExpression and hasName(["evaluate", "prepare"])
or
getDeclaringType() instanceof UnifiedJexlTemplate and hasName("evaluate")
}
}
/**
* A method that creates a JEXL script.
*/
private class CreateJexlScriptMethod extends Method {
CreateJexlScriptMethod() { getDeclaringType() instanceof JexlEngine and hasName("createScript") }
}
/**
* A method that creates a `Callable` for a JEXL expression or script.
*/
private class CreateJexlCallableMethod extends Method {
CreateJexlCallableMethod() {
(getDeclaringType() instanceof JexlExpression or getDeclaringType() instanceof JexlScript) and
hasName("callable")
}
}
/**
* A method that creates a JEXL template.
*/
private class CreateJexlTemplateMethod extends Method {
CreateJexlTemplateMethod() {
(getDeclaringType() instanceof JxltEngine or getDeclaringType() instanceof UnifiedJexl) and
hasName("createTemplate")
}
}
/**
* A method that creates a JEXL expression.
*/
private class CreateJexlExpressionMethod extends Method {
CreateJexlExpressionMethod() {
(getDeclaringType() instanceof JexlEngine or getDeclaringType() instanceof JxltEngine) and
hasName("createExpression")
or
getDeclaringType() instanceof UnifiedJexl and hasName("parse")
}
}
private class JexlRefType extends RefType {
JexlRefType() { getPackage().hasName(["org.apache.commons.jexl2", "org.apache.commons.jexl3"]) }
}
private class JexlExpression extends JexlRefType {
JexlExpression() { hasName(["Expression", "JexlExpression"]) }
}
private class JexlScript extends JexlRefType {
JexlScript() { hasName(["Script", "JexlScript"]) }
}
private class JexlBuilder extends JexlRefType {
JexlBuilder() { hasName("JexlBuilder") }
}
private class JexlEngine extends JexlRefType {
JexlEngine() { hasName("JexlEngine") }
}
private class JxltEngine extends JexlRefType {
JxltEngine() { hasName("JxltEngine") }
}
private class UnifiedJexl extends JexlRefType {
UnifiedJexl() { hasName("UnifiedJEXL") }
}
private class JexlUberspect extends Interface {
JexlUberspect() {
hasQualifiedName("org.apache.commons.jexl2.introspection", "Uberspect") or
hasQualifiedName("org.apache.commons.jexl3.introspection", "JexlUberspect")
}
}
private class JxltEngineExpression extends NestedType {
JxltEngineExpression() { getEnclosingType() instanceof JxltEngine and hasName("Expression") }
}
private class JxltEngineTemplate extends NestedType {
JxltEngineTemplate() { getEnclosingType() instanceof JxltEngine and hasName("Template") }
}
private class UnifiedJexlExpression extends NestedType {
UnifiedJexlExpression() { getEnclosingType() instanceof UnifiedJexl and hasName("Expression") }
}
private class UnifiedJexlTemplate extends NestedType {
UnifiedJexlTemplate() { getEnclosingType() instanceof UnifiedJexl and hasName("Template") }
}
private class Reader extends RefType {
Reader() { hasQualifiedName("java.io", "Reader") }
}

View File

@@ -3,6 +3,8 @@
* @description Evaluation of a user-controlled malicious expression in Java Python
* interpreter may lead to remote code execution.
* @kind path-problem
* @problem.severity error
* @precision high
* @id java/jython-injection
* @tags security
* external/cwe/cwe-094

View File

@@ -1,14 +0,0 @@
public void evaluate(Socket socket) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
JexlSandbox onlyMath = new JexlSandbox(false);
onlyMath.white("java.lang.Math");
JexlEngine jexl = new JexlBuilder().sandbox(onlyMath).create();
String input = reader.readLine();
JexlExpression expression = jexl.createExpression(input);
JexlContext context = new MapContext();
expression.evaluate(context);
}
}

View File

@@ -1,90 +0,0 @@
public void evaluate(Socket socket) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
JexlUberspect sandbox = new JexlUberspectSandbox();
JexlEngine jexl = new JexlBuilder().uberspect(sandbox).create();
String input = reader.readLine();
JexlExpression expression = jexl.createExpression(input);
JexlContext context = new MapContext();
expression.evaluate(context);
}
private static class JexlUberspectSandbox implements JexlUberspect {
private static final List<String> ALLOWED_CLASSES =
Arrays.asList("java.lang.Math", "java.util.Random");
private final JexlUberspect uberspect = new JexlBuilder().create().getUberspect();
private void checkAccess(Object obj) {
if (!ALLOWED_CLASSES.contains(obj.getClass().getCanonicalName())) {
throw new AccessControlException("Not allowed");
}
}
@Override
public JexlMethod getMethod(Object obj, String method, Object... args) {
checkAccess(obj);
return uberspect.getMethod(obj, method, args);
}
@Override
public List<PropertyResolver> getResolvers(JexlOperator op, Object obj) {
checkAccess(obj);
return uberspect.getResolvers(op, obj);
}
@Override
public void setClassLoader(ClassLoader loader) {
uberspect.setClassLoader(loader);
}
@Override
public int getVersion() {
return uberspect.getVersion();
}
@Override
public JexlMethod getConstructor(Object obj, Object... args) {
checkAccess(obj);
return uberspect.getConstructor(obj, args);
}
@Override
public JexlPropertyGet getPropertyGet(Object obj, Object identifier) {
checkAccess(obj);
return uberspect.getPropertyGet(obj, identifier);
}
@Override
public JexlPropertyGet getPropertyGet(List<PropertyResolver> resolvers, Object obj, Object identifier) {
checkAccess(obj);
return uberspect.getPropertyGet(resolvers, obj, identifier);
}
@Override
public JexlPropertySet getPropertySet(Object obj, Object identifier, Object arg) {
checkAccess(obj);
return uberspect.getPropertySet(obj, identifier, arg);
}
@Override
public JexlPropertySet getPropertySet(List<PropertyResolver> resolvers, Object obj, Object identifier, Object arg) {
checkAccess(obj);
return uberspect.getPropertySet(resolvers, obj, identifier, arg);
}
@Override
public Iterator<?> getIterator(Object obj) {
checkAccess(obj);
return uberspect.getIterator(obj);
}
@Override
public JexlArithmetic.Uberspect getArithmetic(JexlArithmetic arithmetic) {
return uberspect.getArithmetic(arithmetic);
}
}
}

View File

@@ -1,11 +0,0 @@
public void evaluate(Socket socket) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
String input = reader.readLine();
JexlEngine jexl = new JexlBuilder().create();
JexlExpression expression = jexl.createExpression(input);
JexlContext context = new MapContext();
expression.evaluate(context);
}
}

View File

@@ -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));
}

View File

@@ -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 {
// ...
}

View File

@@ -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 {
// ...
}

View File

@@ -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 &amp; 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>

View File

@@ -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."