An Easy Way to Secure Java Applications

One of the things that stands out in the Java Serialization exploit is that once a server side Java application is compromised, the next step is to gain shell access on the host machine. This is known as a Remote Code Execution, or RCE for short.

The interesting thing is that Java has had a way to restrict execution and prevent RCE almost since Java 1.1: the SecurityManager. With the SecurityManager enabled, Java code operates inside a far more secure sandbox that prevents RCE.

java -Djava.security.manager com.example.Hello

This runs with the default security policy in $JAVA_HOME/jre/lib/security/java.policy, which in JDK 1.8 is:

// Standard extensions get all permissions by default

grant codeBase "file:$/*" {
        permission java.security.AllPermission;
};

// default permissions granted to all domains

grant {
        // Allows any thread to stop itself using the java.lang.Thread.stop()
        // method that takes no argument.
        // Note that this permission is granted by default only to remain
        // backwards compatible.
        // It is strongly recommended that you either remove this permission
        // from this policy file or further restrict it to code sources
        // that you specify, because Thread.stop() is potentially unsafe.
        // See the API specification of java.lang.Thread.stop() for more
        // information.
        permission java.lang.RuntimePermission "stopThread";

        // allows anyone to listen on dynamic ports
        permission java.net.SocketPermission "localhost:0", "listen";

        // "standard" properies that can be read by anyone

        permission java.util.PropertyPermission "java.version", "read";
        permission java.util.PropertyPermission "java.vendor", "read";
        permission java.util.PropertyPermission "java.vendor.url", "read";
        permission java.util.PropertyPermission "java.class.version", "read";
        permission java.util.PropertyPermission "os.name", "read";
        permission java.util.PropertyPermission "os.version", "read";
        permission java.util.PropertyPermission "os.arch", "read";
        permission java.util.PropertyPermission "file.separator", "read";
        permission java.util.PropertyPermission "path.separator", "read";
        permission java.util.PropertyPermission "line.separator", "read";

        permission java.util.PropertyPermission "java.specification.version", "read";
        permission java.util.PropertyPermission "java.specification.vendor", "read";
        permission java.util.PropertyPermission "java.specification.name", "read";

        permission java.util.PropertyPermission "java.vm.specification.version", "read";
        permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
        permission java.util.PropertyPermission "java.vm.specification.name", "read";
        permission java.util.PropertyPermission "java.vm.version", "read";
        permission java.util.PropertyPermission "java.vm.vendor", "read";
        permission java.util.PropertyPermission "java.vm.name", "read";
};

Take code like this, for example:

package com.example

object Hello {
  def main(args: Array[String]): Unit = {
    val runtime = Runtime.getRuntime
    val cwd = System.getProperty("user.dir")
    val process = runtime.exec(s"$cwd/testscript.sh")
    println("Process executed without security manager interference!")
  }
}

With the security manager enabled and using an additional policy file, it's possible to enable or disable execute privileges cleanly:

grant {
  // You can read user.dir
  permission java.util.PropertyPermission "user.dir", "read";

  // Gets access to the current user directory script
  permission java.io.FilePermission "${user.dir}/testscript.sh", "execute";
  permission java.util.PropertyPermission "scala.control.noTraceSuppression", "read";
};

You can run this with:

java -Djava.security.manager -Djava.security.policy=security.policy com.example.Hello

Just comment out the FilePermission line, and you get an exception.

So far, so great. But it's only enabled for applets, on the client side. It's disabled on the server-side.

Why? Well, because (as you can see above) the default SecurityManager locks down the system to the point of uselessness. In order to make the system useful, it must have a custom java.security.policy file defined.

This policy implementation has several problems. The policy file itself is archaic. The security permissions are not laid out in any kind of logical order, and some permissions have options for wildcards while others do not. You can only "allow" behavior with whitelists, not deny it. And worst of all, the longer the list, the slower the application will run. There is a tutorial and list of permissions, but it's not terribly helpful in practice. And the documentation guide was last updated in 2002.

It is possible to write a custom SecurityManager when you have untrusted code: this is what Scalatron does, for example. NOTE: This is NOT a secure implementation of a sandbox according to Ben Murphy.

However, if we want to prevent RCE, then we want a general purpose SecurityManager that allows almost everything, but can prevent scripts being run on the host. It wouldn't be a perfect defense, but it would be a decent part of a defense in depth strategy.

It turns out that someone already did this!

Ondrej Lukas put together pro-grade, which adds a "deny" option as well as an "allow" option to Java policy files. There's a presentation at Devoxx going over pro-grade, with slides and video.

Now, using pro-grade with the previous example, the following policy would lock down all execution access:

priority "grant";

deny {
  // https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html#FilePermission
  permission java.io.FilePermission "<<ALL FILES>>", "execute";
};

Note that this is not a complete solution. I suspect you would need to deny several other permissions in addition to this to prevent code from working around this, and I don't know which ones are relevant for a blacklist. But it's a start, and it goes a long way towards hardening Java server-side applications very cheaply.

EDIT: Have done research, and you do need to set additional permissions to prevent the SecurityManager itself from being circumvented. Please see this post for the relevant permissions.

Pro-grade is very simple to set up with an appropriate policy. There's a policy generator that can show all the needed permissions for an application, and a tutorial showing all the steps needed to set it up, and a permissions debugger to catch stray permissions after that.

The example project is available at prograde-example, and pro-grade can be integrated into your project from http://mvnrepository.com/artifact/net.sourceforge.pro-grade/pro-grade/1.1.1. Most people will want Maven:

<dependency>
	<groupId>net.sourceforge.pro-grade</groupId>
	<artifactId>pro-grade</artifactId>
	<version>1.1.1</version>
</dependency>

The really interesting thing about pro-grade is that it's a transparent solution. While it's nice to have a whitelist policy, using this technique you can add pro-grade onto an existing, already compiled project, and deny script execution.

With only minor modifications, pro-grade could be made to notify of violations (especially in setSecurityManager and other seldom touched areas), and appear to work while also silently mangling the operation elsewhere. All you need to do is implement PermissionDeniedListener with an SLF4J implementation.

There's enough research in attacks, it's nice to see some progress being made in defense as well.

Comments