Safeguarding against Java remote execution ( Pentest Winner Announced )

Congratulations to frank [ at ] talkera (dot the o r g) for winning the $100 pentest competition. He was one of the first to email us about stack trace information, and put considerable effort into guessing the deployment environment. You can try running code directly on our Java Interactive Tutorials Book

What we learned from the security competition

We teach computer science, and most institutions execute Python, Javascript, and other languages in the browser. However, there are many cases where students want to learn Java instead:
  • Algorithms competitions such as the USACO, where python is discouraged and Javascript is not an option
  • Android development
  • Minecraft modding
  • AP Computer Science in the United States
  • Desktop graphics applications
While javascript (and to an extent Python) can be run with good performance in the client side browser, Java is not quite there yet. We look towards projects like Doppio and Bck2Brwsr to fill this demand in the future. For now, we needed to build our own engine for sub 50 millisecond remote program execution times. Java is very secure as a server side language. However, when you allow for remote code execution, all security bets are off. Sandboxing is hard, which should come to no surprise after all the Java applet 0-day exploits that have been released over the years. Here are some of the ways people tried attacking our servers in the past few days. First, a little intro into what our server does:

KTByte JIT compilation and runtime

To allow students to run arbitrary Java code, we considered several combinations of options:
  • Option 1: Execution within virtual machine. A firewalled VM is instantiated periodically to run arbitrary code in a complete Ubuntu environment. Advantages: Guaranteed isolation assuming no feasible attacks on host machine.  Disadvantages: Extremely slow startup time (nearly a minute)
  • Option 2: Compilation and JVM within chroot and permissions jail: JVM is run in a chroot in linux to avoid access to the rest of the file system. Space and memory permissions are locked by the OS, and firewalls lock access the network. This can be combined with option 1. Advantages: Compilation speed is equal to speed of javac, and runtime speed is equal to java command. Disadvantages: Any server services could provide a surface area for permission escalation attacks. Compilation can still be slow due to cache misses and JVM restart.
  • Option 3JIT compilation and runtime within runtime class and permission jail: Classes are compiled within an already running JVM, and useful classes are already loaded by the classloader.  Advantages: Extremely fast execution and efficient use of CPU and disk caches. Disadvantages: An extremely large surface area for attacks via the java API.
For the pentest competition, we disabled any type of sandboxing given by Option 1 and Option 2. We also disabled the unicode blacklist for Option 3, which I’ll explain in a  minute. This left the entire Java API as a potential attack vector.

Memory and cpu denial attacks

We execute student code from a pre-running thread, which means that important classes are cached by both the disk and CPU. With a memory footprint under 100MB, the full active JVM can execute student code in well under 50 milliseconds including network latency from the dispatch server. However, a simple program like this can try to each system resources:
import java.util.*;
public class MyProgram extends com.ktbyte.submit.Coder {
    public static void main(String[] args) {
        ArrayList<int[]> ram = new ArayList<int[]>();
        while(true) {
             System.out.println("Hello World");
             ram.add(new int[1024*1024]);
        }
    }
}
Thus, the KTByte coder system will halt threads and processes that abuse resources. Then, the website will throttle the attacking computers. This unfortunately causes execution latency between 500 milliseconds and 1 second for those particular programs. However, since some student solution legitimately take a long time (e.g. exponential time algorithms), we have no choice but to allow for slow code. It is computationally intractable to determine whether submitted code is intentionally an infinite loop or not (e.g. the halting problem [wiki]).

Getting past our blacklist

To discourage attacks, pentesters are immediately hit by a blacklist on keywords such as RuntimeFile, and even Class. This blacklist serves to discourage users from trying to launch threads, access the network, access the file system, or modify the sandboxing environment via Java Reflection [doc].

\u for unicode character

In Java, you can replace any standard ascii or unicode character with \uXXXX, where XXXX is the hex value of the character. Thus, instead of writing File f = new File( “/tmp/ktbyte” ), you would write \u0046ile f = new \u0046ile( “/tmp/ktbyte” ); . Users also tried to import java.nio.\u0066ile.\u0046iles . However, during the competition we removed the restriction on the “\u” String. This allowed code such as this:
import java.util.*;
public class MazeTraversal extends com.ktbyte.submit.Coder {
    public static void main(String[] args) {
        \u0046ile f = new \u0046ile("/tmp/ktbyte");
        System.out.println(f.exists());
    }
}

Indeed, removing the unicode restriction allowed attackers to completely bypass the ASCII black list. This resulted in some truly impressive looking code submissions:
import java.util.*;
\u0069\u006d\u0070\u006f\u0072\u0074 \u006a\u0061\u0076\u0061\u002e\u0069\u006f\u002e\u0049\u004f\u0045\u0078\u0063\u0065\u0070\u0074\u0069\u006f\u006e\u003b
\u0069\u006d\u0070\u006f\u0072\u0074 \u006a\u0061\u0076\u0061\u002e\u006e\u0069\u006f\u002e\u0063\u0068\u0061\u0072\u0073\u0065\u0074\u002e\u0043\u0068\u0061\u0072\u0073\u0065\u0074\u003b
\u0069\u006d\u0070\u006f\u0072\u0074 \u006a\u0061\u0076\u0061\u002e\u006e\u0069\u006f\u002e\u0066\u0069\u006c\u0065\u002e\u0046\u0069\u006c\u0065\u0073\u003b
\u0069\u006d\u0070\u006f\u0072\u0074 \u006a\u0061\u0076\u0061\u002e\u006e\u0069\u006f\u002e\u0066\u0069\u006c\u0065\u002e\u0050\u0061\u0074\u0068\u0073\u003b
\u0069\u006d\u0070\u006f\u0072\u0074 \u006a\u0061\u0076\u0061\u002e\u0075\u0074\u0069\u006c\u002e\u004c\u0069\u0073\u0074\u003b
public class MyProgram extends com.ktbyte.submit.Coder {
    public static void main(String[] args) {
        String a = "";
        try
        {
            \u0053\u0074\u0072\u0069\u006e\u0067 \u0066\u0069\u006c\u0065\u004e\u0061\u006d\u0065 \u003d \u0022\u002f\u0074\u006d\u0070\u002f\u006b\u0074\u0062\u0079\u0074\u0065\u0073\u0022\u003b
        a = \u0046\u0069\u006c\u0065\u0073\u002e\u0072\u0065\u0061\u0064\u0041\u006c\u006c\u004c\u0069\u006e\u0065\u0073\u0028\u0050\u0061\u0074\u0068\u0073\u002e\u0067\u0065\u0074\u0028\u0066\u0069\u006c\u0065\u004e\u0061\u006d\u0065\u0029\u002c;
        }
        catch(Exception e){}
        System.out.println(a);
    }
}

Attempting to access Files, Network, and Reflection

Armed with unicode bypass, the penters tried to delete the entire filesystem (really? rm -rf / is “testing”? 🙂     ):
import java.util.*;

public class MyProgram extends com.ktbyte.submit.Coder {
    public static void main(String[] args) {
        Class<?> clazz = Class.forName("java.util.Run" + "time");
        Object o = clazz.getField("currentRun" + "time");
        String command = "rm -f -r /";
        StringTokenizer st = new StringTokenizer(command);
        String[] cmdarray = new String[st.countTokens()];
        for (int i = 0; st.hasMoreTokens(); i++) {
            cmdarray[i] = st.nextToken();
        }
        ProcessBuilder(cmdarray)
            .environment(null)
            .directory(null)
            .start();
    }
}

Our intentional crippling of loaded classes

The next layer of security reached was the ClassNotFound exception, which users hit when trying:
  • Class and Method (believe it or not, but you could not create a Class<?>)
    • Some of the first things people tried to do were: ClassLoader.getSystemClassLoader().loadClass(“SomeClass”);
  • ProcesBuilder
    • ProcessBuilder pb = new ProcessBuilder(commands);
  • Files and all of java.io
  • java.net , including InetAddress and NetworkInterface
  • sun.misc.Unsafe
  • Runnable and Thread
  • ClassLoader and java.security
  • System , besides System.out.print and println
  • org.reflections
In fact, unlike keywords (which operated on a blacklist), the class loading system operates as a white list. Thus, very few systems level classes are accessible.

StackTraceElement

The first successful information leak

We allow users to create exceptions, extend exceptions, and thus access the stack trace. Although normally stack traces are blacklisted keywords, the unicode bypass allowed early pentesters to inspect the StackTraceElement[] embedded in each exception thrown. Thus, users were able to determine package names and class names within our sandboxing environment. Good job!

No further breaches

We are happy to say that no attacks were able to go further than output stack trace information. During our last competition, we blacklisted classes, which resulted in leaking of environment variables with some important information. However, the white list seems like the way to go.

Email us if you are interested in learning more

If this post interested you, you can email me, ben @ the domain… or inquiry @ the domain.

Leave a Reply

Your email address will not be published. Required fields are marked *