Skip to content

Commit 4709e81

Browse files
committed
JPython code injection
1 parent b2c0259 commit 4709e81

File tree

13 files changed

+750
-1
lines changed

13 files changed

+750
-1
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
public class JPythonInjection extends HttpServlet {
2+
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
3+
response.setContentType("text/plain");
4+
String code = request.getParameter("code");
5+
PythonInterpreter interpreter = null;
6+
ByteArrayOutputStream out = new ByteArrayOutputStream();
7+
8+
try {
9+
interpreter = new PythonInterpreter();
10+
interpreter.setOut(out);
11+
interpreter.setErr(out);
12+
13+
// BAD: allow arbitrary JPython expression to execute
14+
interpreter.exec(code);
15+
out.flush();
16+
17+
response.getWriter().print(out.toString());
18+
} catch(PyException ex) {
19+
response.getWriter().println(ex.getMessage());
20+
} finally {
21+
if (interpreter != null) {
22+
interpreter.close();
23+
}
24+
out.close();
25+
}
26+
}
27+
28+
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
29+
response.setContentType("text/plain");
30+
String code = request.getParameter("code");
31+
PythonInterpreter interpreter = null;
32+
33+
try {
34+
interpreter = new PythonInterpreter();
35+
// BAD: allow arbitrary JPython expression to evaluate
36+
PyObject py = interpreter.eval(code);
37+
38+
response.getWriter().print(py.toString());
39+
} catch(PyException ex) {
40+
response.getWriter().println(ex.getMessage());
41+
} finally {
42+
if (interpreter != null) {
43+
interpreter.close();
44+
}
45+
}
46+
}
47+
}
48+
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 JPython
8+
is a popular Java implementation of Python. It allows embedded Python scripting inside
9+
Java applications and provides an interactive interpreter that can be used to interact
10+
with Java packages or with running Java applications. If an expression is built using
11+
attacker-controlled data and then evaluated, it may allow the attacker to run arbitrary
12+
code.</p>
13+
</overview>
14+
15+
<recommendation>
16+
<p>In general, including user input in JPython 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 random code in JPython Interpreter</p>
23+
<sample src="JPythonInjection.java" />
24+
</example>
25+
26+
<references>
27+
<li>
28+
JPython Organization: <a href="https://jython.readthedocs.io/en/latest/JythonAndJavaIntegration/">JPython 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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @name Injection in JPython
3+
* @description Evaluation of a user-controlled malicious expression in JPython
4+
* may lead to remote code execution.
5+
* @kind path-problem
6+
* @id java/jpython-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 DataFlow::PathGraph
15+
16+
/** The class `org.python.util.PythonInterpreter`. */
17+
class PythonInterpreter extends RefType {
18+
PythonInterpreter() { this.hasQualifiedName("org.python.util", "PythonInterpreter") }
19+
}
20+
21+
/** A method that evaluates, compiles or executes a JPython expression. */
22+
class InterpretExprMethod extends Method {
23+
InterpretExprMethod() {
24+
this.getDeclaringType().getAnAncestor*() instanceof PythonInterpreter and
25+
(
26+
hasName("exec") or
27+
hasName("eval") or
28+
hasName("compile")
29+
)
30+
}
31+
}
32+
33+
/** Holds if a JPython expression if evaluated, compiled or executed. */
34+
predicate runCode(MethodAccess ma, Expr sink) {
35+
exists(Method m | m = ma.getMethod() |
36+
m instanceof InterpretExprMethod and
37+
sink = ma.getArgument(0)
38+
)
39+
}
40+
41+
/** Sink of an expression interpreted by JPython interpreter. */
42+
class CodeInjectionSink extends DataFlow::ExprNode {
43+
CodeInjectionSink() { runCode(_, this.getExpr()) }
44+
45+
MethodAccess getMethodAccess() { runCode(result, this.getExpr()) }
46+
}
47+
48+
class CodeInjectionConfiguration extends TaintTracking::Configuration {
49+
CodeInjectionConfiguration() { this = "CodeInjectionConfiguration" }
50+
51+
override predicate isSource(DataFlow::Node source) {
52+
source instanceof RemoteFlowSource
53+
or
54+
source instanceof LocalUserInput
55+
}
56+
57+
override predicate isSink(DataFlow::Node sink) { sink instanceof CodeInjectionSink }
58+
59+
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
60+
// @RequestBody MyQueryObj query; interpreter.exec(query.getInterpreterCode());
61+
exists(MethodAccess ma | ma.getQualifier() = node1.asExpr() and ma = node2.asExpr())
62+
}
63+
}
64+
65+
from DataFlow::PathNode source, DataFlow::PathNode sink, CodeInjectionConfiguration conf
66+
where conf.hasFlowPath(source, sink)
67+
select sink.getNode().(CodeInjectionSink).getMethodAccess(), source, sink, "JPython evaluate $@.",
68+
source.getNode(), "user input"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
edges
2+
| JPythonInjection.java:22:23:22:50 | getParameter(...) : String | JPythonInjection.java:30:28:30:31 | code |
3+
| JPythonInjection.java:47:21:47:48 | getParameter(...) : String | JPythonInjection.java:52:40:52:43 | code |
4+
nodes
5+
| JPythonInjection.java:22:23:22:50 | getParameter(...) : String | semmle.label | getParameter(...) : String |
6+
| JPythonInjection.java:30:28:30:31 | code | semmle.label | code |
7+
| JPythonInjection.java:47:21:47:48 | getParameter(...) : String | semmle.label | getParameter(...) : String |
8+
| JPythonInjection.java:52:40:52:43 | code | semmle.label | code |
9+
#select
10+
| JPythonInjection.java:30:11:30:32 | exec(...) | JPythonInjection.java:22:23:22:50 | getParameter(...) : String | JPythonInjection.java:30:28:30:31 | code | JPython evaluate $@. | JPythonInjection.java:22:23:22:50 | getParameter(...) | user input |
11+
| JPythonInjection.java:52:23:52:44 | eval(...) | JPythonInjection.java:47:21:47:48 | getParameter(...) : String | JPythonInjection.java:52:40:52:43 | code | JPython evaluate $@. | JPythonInjection.java:47:21:47:48 | getParameter(...) | user input |
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import java.io.ByteArrayOutputStream;
2+
import java.io.IOException;
3+
import javax.servlet.ServletException;
4+
import javax.servlet.http.HttpServlet;
5+
import javax.servlet.http.HttpServletRequest;
6+
import javax.servlet.http.HttpServletResponse;
7+
8+
import org.python.core.PyObject;
9+
import org.python.core.PyException;
10+
import org.python.util.PythonInterpreter;
11+
12+
public class JPythonInjection extends HttpServlet {
13+
private static final long serialVersionUID = 1L;
14+
15+
public JPythonInjection() {
16+
super();
17+
}
18+
19+
// BAD: allow arbitrary JPython expression to execute
20+
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
21+
response.setContentType("text/plain");
22+
String code = request.getParameter("code");
23+
PythonInterpreter interpreter = null;
24+
ByteArrayOutputStream out = new ByteArrayOutputStream();
25+
26+
try {
27+
interpreter = new PythonInterpreter();
28+
interpreter.setOut(out);
29+
interpreter.setErr(out);
30+
interpreter.exec(code);
31+
out.flush();
32+
33+
response.getWriter().print(out.toString());
34+
} catch(PyException ex) {
35+
response.getWriter().println(ex.getMessage());
36+
} finally {
37+
if (interpreter != null) {
38+
interpreter.close();
39+
}
40+
out.close();
41+
}
42+
}
43+
44+
// BAD: allow arbitrary JPython expression to evaluate
45+
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
46+
response.setContentType("text/plain");
47+
String code = request.getParameter("code");
48+
PythonInterpreter interpreter = null;
49+
50+
try {
51+
interpreter = new PythonInterpreter();
52+
PyObject py = interpreter.eval(code);
53+
54+
response.getWriter().print(py.toString());
55+
} catch(PyException ex) {
56+
response.getWriter().println(ex.getMessage());
57+
} finally {
58+
if (interpreter != null) {
59+
interpreter.close();
60+
}
61+
}
62+
}
63+
}
64+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE/CWE-094/JPythonInjection.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/jpython-2.7.2
22

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Corporation for National Research Initiatives
2+
package org.python.core;
3+
4+
/**
5+
* A super class for all python code implementations.
6+
*/
7+
public abstract class PyCode extends PyObject
8+
{
9+
abstract public PyObject call(ThreadState state,
10+
PyObject args[], String keywords[],
11+
PyObject globals, PyObject[] defaults,
12+
PyObject closure);
13+
14+
abstract public PyObject call(ThreadState state,
15+
PyObject self, PyObject args[],
16+
String keywords[],
17+
PyObject globals, PyObject[] defaults,
18+
PyObject closure);
19+
20+
abstract public PyObject call(ThreadState state,
21+
PyObject globals, PyObject[] defaults,
22+
PyObject closure);
23+
24+
abstract public PyObject call(ThreadState state,
25+
PyObject arg1, PyObject globals,
26+
PyObject[] defaults, PyObject closure);
27+
28+
abstract public PyObject call(ThreadState state,
29+
PyObject arg1, PyObject arg2,
30+
PyObject globals, PyObject[] defaults,
31+
PyObject closure);
32+
33+
abstract public PyObject call(ThreadState state,
34+
PyObject arg1, PyObject arg2, PyObject arg3,
35+
PyObject globals, PyObject[] defaults,
36+
PyObject closure);
37+
38+
abstract public PyObject call(ThreadState state,
39+
PyObject arg1, PyObject arg2, PyObject arg3, PyObject arg4,
40+
PyObject globals, PyObject[] defaults,
41+
PyObject closure);
42+
43+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Corporation for National Research Initiatives
2+
package org.python.core;
3+
import java.io.*;
4+
5+
/**
6+
* A wrapper for all python exception. Note that the well-known python exceptions are <b>not</b>
7+
* subclasses of PyException. Instead the python exception class is stored in the <code>type</code>
8+
* field and value or class instance is stored in the <code>value</code> field.
9+
*/
10+
public class PyException extends RuntimeException
11+
{
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Corporation for National Research Initiatives
2+
package org.python.core;
3+
4+
import java.io.Serializable;
5+
6+
/**
7+
* All objects known to the Jython runtime system are represented by an instance of the class
8+
* {@code PyObject} or one of its subclasses.
9+
*/
10+
public class PyObject implements Serializable {
11+
}

0 commit comments

Comments
 (0)