Skip to content

Commit 4230869

Browse files
authored
Merge pull request github#5819 from luchua-bc/java/jpython-injection
Java: CWE-094 Jython code injection
2 parents 71f540a + 2a0721b commit 4230869

File tree

19 files changed

+1213
-1
lines changed

19 files changed

+1213
-1
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import org.python.util.PythonInterpreter;
2+
3+
public class JythonInjection extends HttpServlet {
4+
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
5+
response.setContentType("text/plain");
6+
String code = request.getParameter("code");
7+
PythonInterpreter interpreter = null;
8+
ByteArrayOutputStream out = new ByteArrayOutputStream();
9+
10+
try {
11+
interpreter = new PythonInterpreter();
12+
interpreter.setOut(out);
13+
interpreter.setErr(out);
14+
15+
// BAD: allow execution of arbitrary Python code
16+
interpreter.exec(code);
17+
out.flush();
18+
19+
response.getWriter().print(out.toString());
20+
} catch(PyException ex) {
21+
response.getWriter().println(ex.getMessage());
22+
} finally {
23+
if (interpreter != null) {
24+
interpreter.close();
25+
}
26+
out.close();
27+
}
28+
}
29+
30+
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
31+
response.setContentType("text/plain");
32+
String code = request.getParameter("code");
33+
PythonInterpreter interpreter = null;
34+
35+
try {
36+
interpreter = new PythonInterpreter();
37+
// BAD: allow execution of arbitrary Python code
38+
PyObject py = interpreter.eval(code);
39+
40+
response.getWriter().print(py.toString());
41+
} catch(PyException ex) {
42+
response.getWriter().println(ex.getMessage());
43+
} finally {
44+
if (interpreter != null) {
45+
interpreter.close();
46+
}
47+
}
48+
}
49+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>Python has been the most widely used programming language in recent years, and Jython
8+
(formerly known as JPython) is a popular Java implementation of Python. It allows
9+
embedded Python scripting inside Java applications and provides an interactive interpreter
10+
that can be used to interact with Java packages or with running Java applications. If an
11+
expression is built using attacker-controlled data and then evaluated, it may allow the
12+
attacker to run arbitrary code.</p>
13+
</overview>
14+
15+
<recommendation>
16+
<p>In general, including user input in Jython expression should be avoided. If user input
17+
must be included in an expression, it should be then evaluated in a safe context that
18+
doesn't allow arbitrary code invocation.</p>
19+
</recommendation>
20+
21+
<example>
22+
<p>The following code could execute arbitrary code in Jython Interpreter</p>
23+
<sample src="JythonInjection.java" />
24+
</example>
25+
26+
<references>
27+
<li>
28+
Jython Organization: <a href="https://jython.readthedocs.io/en/latest/JythonAndJavaIntegration/">Jython and Java Integration</a>
29+
</li>
30+
<li>
31+
PortSwigger: <a href="https://portswigger.net/kb/issues/00100f10_python-code-injection">Python code injection</a>
32+
</li>
33+
</references>
34+
</qhelp>
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* @name Injection in Jython
3+
* @description Evaluation of a user-controlled malicious expression in Java Python
4+
* interpreter may lead to remote code execution.
5+
* @kind path-problem
6+
* @id java/jython-injection
7+
* @tags security
8+
* external/cwe/cwe-094
9+
* external/cwe/cwe-095
10+
*/
11+
12+
import java
13+
import semmle.code.java.dataflow.FlowSources
14+
import semmle.code.java.frameworks.spring.SpringController
15+
import DataFlow::PathGraph
16+
17+
/** The class `org.python.util.PythonInterpreter`. */
18+
class PythonInterpreter extends RefType {
19+
PythonInterpreter() { this.hasQualifiedName("org.python.util", "PythonInterpreter") }
20+
}
21+
22+
/** A method that evaluates, compiles or executes a Jython expression. */
23+
class InterpretExprMethod extends Method {
24+
InterpretExprMethod() {
25+
this.getDeclaringType().getAnAncestor*() instanceof PythonInterpreter and
26+
getName().matches(["exec%", "run%", "eval", "compile"])
27+
}
28+
}
29+
30+
/** The class `org.python.core.BytecodeLoader`. */
31+
class BytecodeLoader extends RefType {
32+
BytecodeLoader() { this.hasQualifiedName("org.python.core", "BytecodeLoader") }
33+
}
34+
35+
/** Holds if a Jython expression if evaluated, compiled or executed. */
36+
predicate runsCode(MethodAccess ma, Expr sink) {
37+
exists(Method m | m = ma.getMethod() |
38+
m instanceof InterpretExprMethod and
39+
sink = ma.getArgument(0)
40+
)
41+
}
42+
43+
/** A method that loads Java class data. */
44+
class LoadClassMethod extends Method {
45+
LoadClassMethod() {
46+
this.getDeclaringType().getAnAncestor*() instanceof BytecodeLoader and
47+
hasName(["makeClass", "makeCode"])
48+
}
49+
}
50+
51+
/**
52+
* Holds if `ma` is a call to a class-loading method, and `sink` is the byte array
53+
* representing the class to be loaded.
54+
*/
55+
predicate loadsClass(MethodAccess ma, Expr sink) {
56+
exists(Method m, int i | m = ma.getMethod() |
57+
m instanceof LoadClassMethod and
58+
m.getParameter(i).getType() instanceof Array and // makeClass(java.lang.String name, byte[] data, ...)
59+
sink = ma.getArgument(i)
60+
)
61+
}
62+
63+
/** The class `org.python.core.Py`. */
64+
class Py extends RefType {
65+
Py() { this.hasQualifiedName("org.python.core", "Py") }
66+
}
67+
68+
/** A method declared on class `Py` or one of its descendants that compiles Python code. */
69+
class PyCompileMethod extends Method {
70+
PyCompileMethod() {
71+
this.getDeclaringType().getAnAncestor*() instanceof Py and
72+
getName().matches("compile%")
73+
}
74+
}
75+
76+
/** Holds if source code is compiled with `PyCompileMethod`. */
77+
predicate compile(MethodAccess ma, Expr sink) {
78+
exists(Method m | m = ma.getMethod() |
79+
m instanceof PyCompileMethod and
80+
sink = ma.getArgument(0)
81+
)
82+
}
83+
84+
/** An expression loaded by Jython. */
85+
class CodeInjectionSink extends DataFlow::ExprNode {
86+
MethodAccess methodAccess;
87+
88+
CodeInjectionSink() {
89+
runsCode(methodAccess, this.getExpr()) or
90+
loadsClass(methodAccess, this.getExpr()) or
91+
compile(methodAccess, this.getExpr())
92+
}
93+
94+
MethodAccess getMethodAccess() { result = methodAccess }
95+
}
96+
97+
/**
98+
* A taint configuration for tracking flow from `RemoteFlowSource` to a Jython method call
99+
* `CodeInjectionSink` that executes injected code.
100+
*/
101+
class CodeInjectionConfiguration extends TaintTracking::Configuration {
102+
CodeInjectionConfiguration() { this = "CodeInjectionConfiguration" }
103+
104+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
105+
106+
override predicate isSink(DataFlow::Node sink) { sink instanceof CodeInjectionSink }
107+
}
108+
109+
from DataFlow::PathNode source, DataFlow::PathNode sink, CodeInjectionConfiguration conf
110+
where conf.hasFlowPath(source, sink)
111+
select sink.getNode().(CodeInjectionSink).getMethodAccess(), source, sink, "Jython evaluate $@.",
112+
source.getNode(), "user input"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
edges
2+
| JythonInjection.java:28:23:28:50 | getParameter(...) : String | JythonInjection.java:36:30:36:33 | code |
3+
| JythonInjection.java:53:23:53:50 | getParameter(...) : String | JythonInjection.java:58:44:58:47 | code |
4+
| JythonInjection.java:73:23:73:50 | getParameter(...) : String | JythonInjection.java:81:35:81:38 | code |
5+
| JythonInjection.java:97:23:97:50 | getParameter(...) : String | JythonInjection.java:106:61:106:75 | getBytes(...) |
6+
nodes
7+
| JythonInjection.java:28:23:28:50 | getParameter(...) : String | semmle.label | getParameter(...) : String |
8+
| JythonInjection.java:36:30:36:33 | code | semmle.label | code |
9+
| JythonInjection.java:53:23:53:50 | getParameter(...) : String | semmle.label | getParameter(...) : String |
10+
| JythonInjection.java:58:44:58:47 | code | semmle.label | code |
11+
| JythonInjection.java:73:23:73:50 | getParameter(...) : String | semmle.label | getParameter(...) : String |
12+
| JythonInjection.java:81:35:81:38 | code | semmle.label | code |
13+
| JythonInjection.java:97:23:97:50 | getParameter(...) : String | semmle.label | getParameter(...) : String |
14+
| JythonInjection.java:106:61:106:75 | getBytes(...) | semmle.label | getBytes(...) |
15+
| JythonInjection.java:131:40:131:63 | getInputStream(...) | semmle.label | getInputStream(...) |
16+
#select
17+
| JythonInjection.java:36:13:36:34 | exec(...) | JythonInjection.java:28:23:28:50 | getParameter(...) : String | JythonInjection.java:36:30:36:33 | code | Jython evaluate $@. | JythonInjection.java:28:23:28:50 | getParameter(...) | user input |
18+
| JythonInjection.java:58:27:58:48 | eval(...) | JythonInjection.java:53:23:53:50 | getParameter(...) : String | JythonInjection.java:58:44:58:47 | code | Jython evaluate $@. | JythonInjection.java:53:23:53:50 | getParameter(...) | user input |
19+
| JythonInjection.java:81:13:81:39 | runsource(...) | JythonInjection.java:73:23:73:50 | getParameter(...) : String | JythonInjection.java:81:35:81:38 | code | Jython evaluate $@. | JythonInjection.java:73:23:73:50 | getParameter(...) | user input |
20+
| JythonInjection.java:106:29:106:134 | makeCode(...) | JythonInjection.java:97:23:97:50 | getParameter(...) : String | JythonInjection.java:106:61:106:75 | getBytes(...) | Jython evaluate $@. | JythonInjection.java:97:23:97:50 | getParameter(...) | user input |
21+
| JythonInjection.java:131:29:131:109 | compile(...) | JythonInjection.java:131:40:131:63 | getInputStream(...) | JythonInjection.java:131:40:131:63 | getInputStream(...) | Jython evaluate $@. | JythonInjection.java:131:40:131:63 | getInputStream(...) | user input |
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import java.io.ByteArrayOutputStream;
2+
import java.io.IOException;
3+
import java.io.InputStream;
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+
10+
import org.python.core.BytecodeLoader;
11+
import org.python.core.Py;
12+
import org.python.core.PyCode;
13+
import org.python.core.PyException;
14+
import org.python.core.PyObject;
15+
import org.python.util.InteractiveInterpreter;
16+
import org.python.util.PythonInterpreter;
17+
18+
public class JythonInjection extends HttpServlet {
19+
private static final long serialVersionUID = 1L;
20+
21+
public JythonInjection() {
22+
super();
23+
}
24+
25+
// BAD: allow execution of arbitrary Python code
26+
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
27+
response.setContentType("text/plain");
28+
String code = request.getParameter("code");
29+
PythonInterpreter interpreter = null;
30+
ByteArrayOutputStream out = new ByteArrayOutputStream();
31+
32+
try {
33+
interpreter = new PythonInterpreter();
34+
interpreter.setOut(out);
35+
interpreter.setErr(out);
36+
interpreter.exec(code);
37+
out.flush();
38+
39+
response.getWriter().print(out.toString());
40+
} catch(PyException ex) {
41+
response.getWriter().println(ex.getMessage());
42+
} finally {
43+
if (interpreter != null) {
44+
interpreter.close();
45+
}
46+
out.close();
47+
}
48+
}
49+
50+
// BAD: allow execution of arbitrary Python code
51+
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
52+
response.setContentType("text/plain");
53+
String code = request.getParameter("code");
54+
PythonInterpreter interpreter = null;
55+
56+
try {
57+
interpreter = new PythonInterpreter();
58+
PyObject py = interpreter.eval(code);
59+
60+
response.getWriter().print(py.toString());
61+
} catch(PyException ex) {
62+
response.getWriter().println(ex.getMessage());
63+
} finally {
64+
if (interpreter != null) {
65+
interpreter.close();
66+
}
67+
}
68+
}
69+
70+
// BAD: allow arbitrary Jython expression to run
71+
protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
72+
response.setContentType("text/plain");
73+
String code = request.getParameter("code");
74+
InteractiveInterpreter interpreter = null;
75+
ByteArrayOutputStream out = new ByteArrayOutputStream();
76+
77+
try {
78+
interpreter = new InteractiveInterpreter();
79+
interpreter.setOut(out);
80+
interpreter.setErr(out);
81+
interpreter.runsource(code);
82+
out.flush();
83+
84+
response.getWriter().print(out.toString());
85+
} catch(PyException ex) {
86+
response.getWriter().println(ex.getMessage());
87+
} finally {
88+
if (interpreter != null) {
89+
interpreter.close();
90+
}
91+
}
92+
}
93+
94+
// BAD: load arbitrary class file to execute
95+
protected void doTrace(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
96+
response.setContentType("text/plain");
97+
String code = request.getParameter("code");
98+
PythonInterpreter interpreter = null;
99+
ByteArrayOutputStream out = new ByteArrayOutputStream();
100+
101+
try {
102+
interpreter = new PythonInterpreter();
103+
interpreter.setOut(out);
104+
interpreter.setErr(out);
105+
106+
PyCode pyCode = BytecodeLoader.makeCode("test", code.getBytes(), getServletContext().getRealPath("/com/example/test.pyc"));
107+
interpreter.exec(pyCode);
108+
out.flush();
109+
110+
response.getWriter().print(out.toString());
111+
} catch(PyException ex) {
112+
response.getWriter().println(ex.getMessage());
113+
} finally {
114+
if (interpreter != null) {
115+
interpreter.close();
116+
}
117+
}
118+
}
119+
120+
// BAD: Compile Python code to execute
121+
protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
122+
response.setContentType("text/plain");
123+
PythonInterpreter interpreter = null;
124+
ByteArrayOutputStream out = new ByteArrayOutputStream();
125+
126+
try {
127+
interpreter = new PythonInterpreter();
128+
interpreter.setOut(out);
129+
interpreter.setErr(out);
130+
131+
PyCode pyCode = Py.compile(request.getInputStream(), "Test.py", org.python.core.CompileMode.eval);
132+
interpreter.exec(pyCode);
133+
out.flush();
134+
135+
response.getWriter().print(out.toString());
136+
} catch(PyException ex) {
137+
response.getWriter().println(ex.getMessage());
138+
} finally {
139+
if (interpreter != null) {
140+
interpreter.close();
141+
}
142+
}
143+
}
144+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE/CWE-094/JythonInjection.ql
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../../stubs/servlet-api-2.4
1+
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jython-2.7.2
22

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Autogenerated AST node
2+
package org.python.antlr.base;
3+
4+
public abstract class mod {
5+
}

0 commit comments

Comments
 (0)