Skip to content

Commit a5cfdd2

Browse files
authored
Merge pull request github#5467 from p0wn4j/groovy-execute
[Java] CWE-094: Query to detect Groovy Code Injections
2 parents f02c86c + 9bfb0d9 commit a5cfdd2

File tree

17 files changed

+729
-1
lines changed

17 files changed

+729
-1
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
4+
<overview>
5+
<p>
6+
Apache Groovy is a powerful, optionally typed and dynamic language,
7+
with static-typing and static compilation capabilities.
8+
9+
It integrates smoothly with any Java program,
10+
and immediately delivers to your application powerful features,
11+
including scripting capabilities, Domain-Specific Language authoring,
12+
runtime and compile-time meta-programming and functional programming.
13+
14+
If a Groovy script is built using attacker-controlled data,
15+
and then evaluated, then it may allow the attacker to achieve RCE.
16+
</p>
17+
</overview>
18+
19+
<recommendation>
20+
<p>
21+
It is generally recommended to avoid using untrusted input in a Groovy evaluation.
22+
If this is not possible, use a sandbox solution. Developers must also take care that Groovy
23+
compile-time metaprogramming can also lead to RCE: it is possible to achieve RCE by compiling
24+
a Groovy script (see the article "Abusing Meta Programming for Unauthenticated RCE!" linked below).
25+
26+
Groovy's <code>SecureASTCustomizer</code> allows securing source code by controlling what code constructs are permitted.
27+
This is typically done when using Groovy for its scripting or domain specific language (DSL) features.
28+
The fundamental problem is that Groovy is a dynamic language, yet <code>SecureASTCustomizer</code> works by looking at Groovy AST statically.
29+
30+
This makes it very easy for an attacker to bypass many of the intended checks
31+
(see https://kohsuke.org/2012/04/27/groovy-secureastcustomizer-is-harmful/).
32+
Therefore, besides <code>SecureASTCustomizer</code>, runtime checks are also necessary before calling Groovy methods
33+
(see https://melix.github.io/blog/2015/03/sandboxing.html).
34+
35+
It is also possible to use a block-list method, excluding unwanted classes from being loaded by the JVM.
36+
This method is not always recommended, because block-lists can be bypassed by unexpected values.
37+
38+
</p>
39+
</recommendation>
40+
41+
<example>
42+
<p>
43+
The following example uses untrusted data to evaluate a Groovy script.
44+
</p>
45+
<sample src="GroovyInjectionBad.java" />
46+
47+
<p>
48+
The following example uses classloader block-list approach to exclude loading dangerous classes.
49+
</p>
50+
<sample src="GroovyInjectionBlocklist.java" />
51+
52+
</example>
53+
54+
<references>
55+
<li>
56+
Orange Tsai:
57+
<a href="https://blog.orange.tw/2019/02/abusing-meta-programming-for-unauthenticated-rce.html">Abusing Meta Programming for Unauthenticated RCE!</a>.
58+
</li>
59+
<li>
60+
Cédric Champeau:
61+
<a href="https://melix.github.io/blog/2015/03/sandboxing.html">Improved sandboxing of Groovy scripts</a>.
62+
</li>
63+
<li>
64+
Kohsuke Kawaguchi:
65+
<a href="https://kohsuke.org/2012/04/27/groovy-secureastcustomizer-is-harmful/">Groovy SecureASTCustomizer is harmful</a>.
66+
</li>
67+
<li>
68+
Welk1n:
69+
<a href="https://github.com/welk1n/exploiting-groovy-in-Java/">Groovy Injection payloads</a>.
70+
</li>
71+
<li>
72+
Charles Chan:
73+
<a href="https://levelup.gitconnected.com/secure-groovy-script-execution-in-a-sandbox-ea39f80ee87/">Secure Groovy Script Execution in a Sandbox</a>.
74+
</li>
75+
<li>
76+
Eugene:
77+
<a href="https://stringconcat.com/en/scripting-and-sandboxing/">Scripting and sandboxing in a JVM environment</a>.
78+
</li>
79+
</references>
80+
81+
</qhelp>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @name Groovy Language injection
3+
* @description Evaluation of a user-controlled Groovy script
4+
* may lead to arbitrary code execution.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @precision high
8+
* @id java/groovy-injection
9+
* @tags security
10+
* external/cwe/cwe-094
11+
*/
12+
13+
import java
14+
import DataFlow::PathGraph
15+
import GroovyInjectionLib
16+
17+
from DataFlow::PathNode source, DataFlow::PathNode sink, GroovyInjectionConfig conf
18+
where conf.hasFlowPath(source, sink)
19+
select sink.getNode(), source, sink, "Groovy Injection from $@.", source.getNode(),
20+
"this user input"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
public class GroovyInjection {
2+
void injectionViaClassLoader(HttpServletRequest request) {
3+
String script = request.getParameter("script");
4+
final GroovyClassLoader classLoader = new GroovyClassLoader();
5+
Class groovy = classLoader.parseClass(script);
6+
GroovyObject groovyObj = (GroovyObject) groovy.newInstance();
7+
}
8+
9+
void injectionViaEval(HttpServletRequest request) {
10+
String script = request.getParameter("script");
11+
Eval.me(script);
12+
}
13+
14+
void injectionViaGroovyShell(HttpServletRequest request) {
15+
GroovyShell shell = new GroovyShell();
16+
String script = request.getParameter("script");
17+
shell.evaluate(script);
18+
}
19+
20+
void injectionViaGroovyShellGroovyCodeSource(HttpServletRequest request) {
21+
GroovyShell shell = new GroovyShell();
22+
String script = request.getParameter("script");
23+
GroovyCodeSource gcs = new GroovyCodeSource(script, "test", "Test");
24+
shell.evaluate(gcs);
25+
}
26+
}
27+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
public class SandboxGroovyClassLoader extends ClassLoader {
2+
public SandboxGroovyClassLoader(ClassLoader parent) {
3+
super(parent);
4+
}
5+
6+
/* override `loadClass` here to prevent loading sensitive classes, such as `java.lang.Runtime`, `java.lang.ProcessBuilder`, `java.lang.System`, etc. */
7+
/* Note we must also block `groovy.transform.ASTTest`, `groovy.lang.GrabConfig` and `org.buildobjects.process.ProcBuilder` to prevent compile-time RCE. */
8+
9+
static void runWithSandboxGroovyClassLoader() throws Exception {
10+
// GOOD: route all class-loading via sand-boxing classloader.
11+
SandboxGroovyClassLoader classLoader = new GroovyClassLoader(new SandboxGroovyClassLoader());
12+
13+
Class<?> scriptClass = classLoader.parseClass(untrusted.getQueryString());
14+
Object scriptInstance = scriptClass.newInstance();
15+
Object result = scriptClass.getDeclaredMethod("bar", new Class[]{}).invoke(scriptInstance, new Object[]{});
16+
}
17+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* Provides classes and predicates for Groovy Code Injection
3+
* taint-tracking configuration.
4+
*/
5+
6+
import java
7+
import semmle.code.java.dataflow.FlowSources
8+
import semmle.code.java.dataflow.TaintTracking
9+
10+
/** A data flow sink for Groovy expression injection vulnerabilities. */
11+
abstract private class GroovyInjectionSink extends DataFlow::ExprNode { }
12+
13+
/**
14+
* A taint-tracking configuration for unsafe user input
15+
* that is used to evaluate a Groovy expression.
16+
*/
17+
class GroovyInjectionConfig extends TaintTracking::Configuration {
18+
GroovyInjectionConfig() { this = "GroovyInjectionConfig" }
19+
20+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
21+
22+
override predicate isSink(DataFlow::Node sink) { sink instanceof GroovyInjectionSink }
23+
24+
override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
25+
groovyCodeSourceTaintStep(fromNode, toNode)
26+
}
27+
}
28+
29+
/** The class `groovy.lang.GroovyShell`. */
30+
private class TypeGroovyShell extends RefType {
31+
TypeGroovyShell() { this.hasQualifiedName("groovy.lang", "GroovyShell") }
32+
}
33+
34+
/** The class `groovy.lang.GroovyCodeSource`. */
35+
private class TypeGroovyCodeSource extends RefType {
36+
TypeGroovyCodeSource() { this.hasQualifiedName("groovy.lang", "GroovyCodeSource") }
37+
}
38+
39+
/**
40+
* Methods in the `GroovyShell` class that evaluate a Groovy expression.
41+
*/
42+
private class GroovyShellMethod extends Method {
43+
GroovyShellMethod() {
44+
this.getDeclaringType() instanceof TypeGroovyShell and
45+
this.getName() in ["evaluate", "parse", "run"]
46+
}
47+
}
48+
49+
private class GroovyShellMethodAccess extends MethodAccess {
50+
GroovyShellMethodAccess() { this.getMethod() instanceof GroovyShellMethod }
51+
}
52+
53+
/**
54+
* Holds if `fromNode` to `toNode` is a dataflow step from a tainted string to
55+
* a `GroovyCodeSource` instance, i.e. `new GroovyCodeSource(tainted, ...)`.
56+
*/
57+
private predicate groovyCodeSourceTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
58+
exists(ConstructorCall gcscc |
59+
gcscc.getConstructedType() instanceof TypeGroovyCodeSource and
60+
gcscc = toNode.asExpr() and
61+
gcscc.getArgument(0) = fromNode.asExpr()
62+
)
63+
}
64+
65+
/**
66+
* A sink for Groovy Injection via the `GroovyShell` class.
67+
*
68+
* ```
69+
* GroovyShell gs = new GroovyShell();
70+
* gs.evaluate(sink, ....)
71+
* gs.run(sink, ....)
72+
* gs.parse(sink,...)
73+
* ```
74+
*/
75+
private class GroovyShellSink extends GroovyInjectionSink {
76+
GroovyShellSink() {
77+
exists(GroovyShellMethodAccess ma, Argument firstArg |
78+
ma.getArgument(0) = firstArg and
79+
firstArg = this.asExpr() and
80+
(
81+
firstArg.getType() instanceof TypeString or
82+
firstArg.getType() instanceof TypeGroovyCodeSource
83+
)
84+
)
85+
}
86+
}
87+
88+
/** The class `groovy.util.Eval`. */
89+
private class TypeEval extends RefType {
90+
TypeEval() { this.hasQualifiedName("groovy.util", "Eval") }
91+
}
92+
93+
/**
94+
* Methods in the `Eval` class that evaluate a Groovy expression.
95+
*/
96+
private class EvalMethod extends Method {
97+
EvalMethod() {
98+
this.getDeclaringType() instanceof TypeEval and
99+
this.getName() in ["me", "x", "xy", "xyz"]
100+
}
101+
}
102+
103+
private class EvalMethodAccess extends MethodAccess {
104+
EvalMethodAccess() { this.getMethod() instanceof EvalMethod }
105+
106+
Expr getArgumentExpr() { result = this.getArgument(this.getNumArgument() - 1) }
107+
}
108+
109+
/**
110+
* A sink for Groovy Injection via the `Eval` class.
111+
*
112+
* ```
113+
* Eval.me(sink)
114+
* Eval.me("p1", "p2", sink)
115+
* Eval.x("p1", sink)
116+
* Eval.xy("p1", "p2" sink)
117+
* Eval.xyz("p1", "p2", "p3", sink)
118+
* ```
119+
*/
120+
private class EvalSink extends GroovyInjectionSink {
121+
EvalSink() { exists(EvalMethodAccess ma | ma.getArgumentExpr() = this.asExpr()) }
122+
}
123+
124+
/** The class `groovy.lang.GroovyClassLoader`. */
125+
private class TypeGroovyClassLoader extends RefType {
126+
TypeGroovyClassLoader() { this.hasQualifiedName("groovy.lang", "GroovyClassLoader") }
127+
}
128+
129+
/**
130+
* A method in the `GroovyClassLoader` class that evaluates a Groovy expression.
131+
*/
132+
private class GroovyClassLoaderParseClassMethod extends Method {
133+
GroovyClassLoaderParseClassMethod() {
134+
this.getDeclaringType() instanceof TypeGroovyClassLoader and
135+
this.hasName("parseClass")
136+
}
137+
}
138+
139+
private class GroovyClassLoaderParseClassMethodAccess extends MethodAccess {
140+
GroovyClassLoaderParseClassMethodAccess() {
141+
this.getMethod() instanceof GroovyClassLoaderParseClassMethod
142+
}
143+
}
144+
145+
/**
146+
* A sink for Groovy Injection via the `GroovyClassLoader` class.
147+
*
148+
* ```
149+
* GroovyClassLoader classLoader = new GroovyClassLoader();
150+
* Class groovy = classLoader.parseClass(script);
151+
* ```
152+
*
153+
* Groovy supports compile-time metaprogramming, so just calling the `parseClass`
154+
* method is enough to achieve RCE.
155+
*/
156+
private class GroovyClassLoadParseClassSink extends GroovyInjectionSink {
157+
GroovyClassLoadParseClassSink() {
158+
exists(GroovyClassLoaderParseClassMethodAccess ma | ma.getArgument(0) = this.asExpr())
159+
}
160+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import groovy.lang.GroovyClassLoader;
2+
import groovy.lang.GroovyCodeSource;
3+
import groovy.lang.GroovyObject;
4+
5+
import javax.servlet.ServletException;
6+
import javax.servlet.http.HttpServlet;
7+
import javax.servlet.http.HttpServletRequest;
8+
import javax.servlet.http.HttpServletResponse;
9+
import java.io.IOException;
10+
11+
public class GroovyClassLoaderTest extends HttpServlet {
12+
13+
protected void doGet(HttpServletRequest request, HttpServletResponse response)
14+
throws ServletException, IOException {
15+
try {
16+
String script = request.getParameter("script");
17+
final GroovyClassLoader classLoader = new GroovyClassLoader();
18+
Class groovy = classLoader.parseClass(script);
19+
GroovyObject groovyObj = (GroovyObject) groovy.newInstance();
20+
21+
} catch (Exception e) {
22+
// Ignore
23+
}
24+
}
25+
26+
protected void doPost(HttpServletRequest request, HttpServletResponse response)
27+
throws ServletException, IOException {
28+
try {
29+
String script = request.getParameter("script");
30+
final GroovyClassLoader classLoader = new GroovyClassLoader();
31+
GroovyCodeSource gcs = new GroovyCodeSource(script, "test", "Test");
32+
Class groovy = classLoader.parseClass(gcs);
33+
GroovyObject groovyObj = (GroovyObject) groovy.newInstance();
34+
} catch (Exception e) {
35+
// Ignore
36+
}
37+
}
38+
}
39+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import javax.servlet.ServletException;
2+
import javax.servlet.http.HttpServlet;
3+
import javax.servlet.http.HttpServletRequest;
4+
import javax.servlet.http.HttpServletResponse;
5+
import java.io.IOException;
6+
import groovy.util.Eval;
7+
8+
public class GroovyEvalTest extends HttpServlet {
9+
10+
protected void doGet(HttpServletRequest request, HttpServletResponse response)
11+
throws ServletException, IOException {
12+
String script = request.getParameter("script");
13+
Eval.me(script);
14+
}
15+
16+
protected void doPost(HttpServletRequest request, HttpServletResponse response)
17+
throws ServletException, IOException {
18+
String script = request.getParameter("script");
19+
Eval.me("test", "result", script);
20+
}
21+
22+
protected void doPut(HttpServletRequest request, HttpServletResponse response)
23+
throws ServletException, IOException {
24+
String script = request.getParameter("script");
25+
Eval.x("result2", script);
26+
27+
}
28+
29+
protected void doDelete(HttpServletRequest request, HttpServletResponse response)
30+
throws ServletException, IOException {
31+
String script = request.getParameter("script");
32+
Eval.xy("result3", "result4", script);
33+
}
34+
35+
protected void doPatch(HttpServletRequest request, HttpServletResponse response)
36+
throws ServletException, IOException {
37+
String script = request.getParameter("script");
38+
Eval.xyz("result3", "result4", "aaa", script);
39+
}
40+
}
41+

0 commit comments

Comments
 (0)