Dec 16, 2024 12:19:26 PM | Penetration Testing Decompiling and Modifying or Backdooring Java applications for Pen Testing

Learn how to decompile, modify, and recompile Java applications for penetration testing in enterprise environments.

Introduction

Thick client assessments are fairly common in the penetration testing field. In many cases, the applications are accessed via windowed Citrix environments intended to lock down access to the underlying host. Often, the host itself is included in the scope of the pentest in a limited way to facilitate discovery of findings surrounding hardcoded secrets or issues with file permissions.

In these types of assessments, the thick client itself may contain secrets or have variables during runtime which contain data retrieved from remote servers that are not shown to the user but could be valuable to the tester. Additionally, on other types of pentests, it may be advantageous to backdoor Java applications running on a system to compromise users.

Java applications, especially those built in-house, are commonly found in enterprise environments. The ability to decompile (i.e. retrieve source code from) and examine or modify these applications gives a significant advantage to any pentester.

During a recent thick client pentest, I was able to use these approaches to decompile and recompile a Java application and gain access to information that the UI did not make available but which provided access to remote databases used by the system. As a result, I felt it might be helpful to share these techniques in the hope that it might be useful to other penetration testers and vulnerability researchers.

Goals

For this post, let’s assume we have a thick client application being used frequently by an enterprise organization on a shared host. We are given access to that host for the purpose of using this application as a normal user.

There are two potential sub-scenarios that we should consider. The first is that we can execute the application but are not able to modify it. The second is the ability to modify the original application and potentially compromise other users. For brevity’s sake, we’ll use the latter for this demonstration.

For demonstration purposes, as a victim thick client we’ll use an application called ImageJ (https://imagej.net/ij/), an open-source medical imaging application written in Java. It’s important to note that we’re targeting the release version of this application, not the source code itself. I chose an open-source application to avoid any potential legal issues with decompiling a commercial application for this post.

ImageJ Website

Running the victim application

We’ll begin by examining our target application. Java applications come in many forms but, often, thick clients will be executed as JAR (Java Archive) files using the Java runtime environment.

After downloading the release version of ImageJ from their website, we’ll extract it in a new folder in a Kali VM. We can then run it using the following syntax:

java -jar ij.jar

The ImageJ application is fairly simple, which is fine for our purposes:

Browsing around the application, we see there is plenty of interesting functionality. One example is the link to the ImageJ website in the Help menu:

If we backdoor the application, there are many options for exploiting a user, but various application triggers such as these are good candidates. Although it may not necessarily be common for a user to click the website link in the Help menu, it serves as an example of application functionality which could be modified for malicious use.

Decompilation

 To decompile the victim application, we’ll use a program called JD-GUI (https://java-decompiler.github.io/).

JD-GUI Website

JD-GUI is packaged as a single JAR file and can be easily run from the command line as demonstrated earlier with the victim application:

Using the “Open File” menu option, we can navigate to the ImageJ’s main JAR file location and open it in JD-GUI:

The interface allows us to see the contents of the JAR file in the pane on the left side in a tree format. As an aside, it’s important to understand that a JAR file is essentially an archive of all .class files and adjacent supporting files used in a Java application.

Finding Secrets

One of the first tasks for these types of thick client assessments is to examine the application for any hardcoded secrets. It’s prudent to examine any property files within the JAR file as well as to perform a search for relevant terms:

When searching for hardcoded secrets, make sure to check all of the options under “Search For” in the search dialog window in order to catch all possible references. Common search terms are “password”, “pwd”, “passwd”, “username”, “jdbc”, etc.

In our case, the victim application is an open-source utility and doesn’t have any secrets exposed.

Modifying functionality

The next step is to modify functionality. This may be to capture variables within the application or to change application behavior. We can do this by copying the decompiled source code, modifying it, recompiling, and then updating the relevant .class file within the victim application’s JAR file.

Let’s modify the functionality of the “ImageJ Website” menu item we identified earlier to perform a malicious action when the victim user clicks it. In order to find the code that is called when the menu item is clicked, we need to be able to identify the action within the decompiled source code. In this case, we know that the text in the menu item is “ImageJ Website”, so we’ll search for that:

We find a hit in the “Menus.class” file which seems appropriate. Clicking the file in the search results opens it in the main JD-GUI pane and brings us to the relevant section of the code. We can see that we’ve found the right place: the code is adding a menu item which then calls a class called “ij.plugin.BrowserLauncher” without any parameters.

If we browse in the source tree in the left pane of JD-GUI we can find the source code for the BrowserLauncher class by visiting the “ij” node, then the “plugin” node under it:

We can see from the code that when the “run()” method is called for the class, if the “theURL” parameter does not exist or is empty, the code will then open the default website for ImageJ.

Now that we’ve found the right code, let’s change it to suit our needs. First, we’ll copy the source content of this file and paste into a text editor. We can then comment out the original URL string value and add our own:

We’ll save the file as BrowserLauncher.java in a folder separate from the rest of the ImageJ application.

Compilation

Our next task is to recompile the source code and update the JAR file with the new .class file generated by the compiler. The above .java file we created is our new source code file. However, this code depends on other classes and libraries used by the ImageJ application. Because of that, we’ll need to include the ij.jar file in the CLASSPATH parameter when compiling the source.

The CLASSPATH parameter allows the user to specify the locations of other JAR files which are used by the classes being compiled. If, when recompiling decompiled code, you run into issues where the compiler complains about missing references, you will need to discover which JAR file in your original program contains the referenced classes and include it in your CLASSPATH argument. Multiple files are separated by colons (“:”) in the CLASSPATH string.

In our case, we’ll only need the one “ij.jar” file in our CLASSPATH. To compile the code, we’ll use the following syntax:

javac -classpath ../../ImageJ/ij.jar BrowserLauncher.java

In the command above, the path given in the “classpath” parameter points to the original ImageJ JAR file and the final parameter is our modified source code file.

At this point we shouldn’t have any errors and our command should result in the creation of a compiled .class file:

Now we need to update the JAR file and replace the original class file inside (remember, it’s an archive) with our new one. However, it’s good practice to do this on a backup copy of the original JAR to avoid accidentally damaging it beyond repair.

We’ll copy the original JAR file to a new one in the folder that contains our modified source code and newly created class file:

At this point, we will need to account for one of the idiosyncrasies of the JAR update process. We need to place our .class file in a folder structure that imitates the package name given in the source code we’re using.

While a comprehensive explanation of Java development isn’t appropriate here, I can provide some insight. Java packages are a group of related classes, which helps to organize the source in a Java project. Java source code files include their package name at the very beginning. In our case, we can see the package name is “ij.plugin”:

In order for Java to update the JAR file properly, we need to place the .class file we created in a similar folder structure to where it would lie within the JAR file itself. To do that, we’ll create a folder called “ij” with a folder called “plugin” within it. We’ll then place our compiled .class file in that folder:

From here, we can update the copied JAR file using the following syntax:

jar uf ./ij_modified.jar ij/plugin/BrowserLauncher.class

If we’re lucky, the command runs successfully and we have a newly modified JAR file:

The next step is to copy the modified JAR file to the ImageJ folder for execution. On a pentest, we may not have the necessary write access to do this. As a result, one approach is to copy the entire application folder to somewhere you do have write access, such as /tmp on a Linux system or c:\windows\temp or c:\temp on a Windows system. Then you should be able to replace the original JAR file with the modified one.

To test our changes, we’ll copy the modified JAR file alongside the original ImageJ file and run it:

Please note, as mentioned previously, it’s important to keep a backup of the original JAR file. This will not only allow you to recover from bad modifications, but also to return the client’s environment to an original state at the end of the assessment.

Success! Our application runs correctly:

If it does not run correctly, or you run into other problems, see the Complications and Concerns section of this post for some tips.

Let’s test out the modified functionality by clicking the “ImageJ Website” menu option shown above. As we can see, our modification is successful:

At this point, if you are intending to exploit other users, it’s time to backup and replace the original JAR file, assuming you have write access to the original application folder:

This application modification approach can be used in the same way to access sensitive application variables in-transit and write the data to a log file or to send web requests to a remote server.

Let’s do one final example to illustrate. If we look at the ImageJ classes in JD-GUI we notice that there is a Prefs class which contains all of the central variables and preferences for the application. Often, in pentested Java applications, these types of variables will contain sensitive information such as passwords or database connection strings. For demonstration’s sake, let’s pretend that the Images URL contains a connection string to some secure image resource:

However, when we use the thick client application normally, we’re never shown this information. To gain access to this data, we can modify the application code in a similar manner to the approach shown above and capture that variable’s value during code execution. Then we can output this value to the console or via another avenue to retrieve the data.

If we examine the code from the Prefs class, we can see that it is a static object (meaning there is one instance across the application). Using other examples within the code, we can access data within the Prefs class by calling it in this way (using our images URL accessor as an example): String somevariable = Prefs.getImagesURL();

In our modified BrowserLauncher class, we’ll need to add a reference to the Prefs class in order to access the methods it has:

We can then adjust our code to call the images URL accessor method and retrieve the value, then output it to the console:

After recompilation and updating the JAR as before, we run the application and click the ImageJ Website menu item. We now have the sensitive data output to the console we ran the application from, allowing us to access it:

We’ve succeeded in grabbing hidden application data which was not intended for a normal user to access, potentially allowing us an avenue to pivot elsewhere in the network.

Complications and Concerns

The process of decompilation and recompilation of Java applications is not always smooth. One issue that often raises its head is that of obfuscated code. While rare in internally developed enterprise applications, it’s not uncommon for commercial products to obfuscate their code when packaging it into JAR files for distribution. As a result, when trying to decompile the code, you get something that looks similar to this:

While a pain to follow the connections between classes, it is usually still possible to recompile these applications with a bit of effort. Sometimes, however, they will cause compilation problems. Such issues are beyond the scope of this post, but it is useful to know that you may encounter these problems when trying to recompile modified Java code.

A second, more general issue is the existence of compilation errors. Anyone who has developed an application (especially in C) knows the pain of troubleshooting compilation errors. While, again, compilation troubleshooting is out of scope for this post, it is good to be aware that encountering these problems is natural. On the bright side, there is extensive documentation and resources on the internet for dealing with various Java compilation error messages. ChatGPT and other AI options may also provide some assistance, although it’s essential to ensure you do not include client data when submitting questions to these services.

One final issue which you may encounter is that of release compatibility. If, after updating the JAR file, you receive an error message about “unsupported versions”, your compiler is likely newer than the Java version in which the JAR file is being run. If this is the case, you can use the “--release <number>” parameter when compiling to specify a target version of the Java runtime when compiling.

Conclusion

This post does not intend to be a comprehensive guide for decompilation, reverse-engineering, software engineering or anything of the sort. However, as a penetration tester, I’ve found these techniques to be useful and have performed each one of these steps on actual real-world assessments with significant results. I hope they will also prove useful to you, the reader, on your own pentesting or red teaming assessments in the future.

 

Written By Justin Benjamin: Justin Benjamin