Frida on Java applications and applets in 2024

As explained in Federico‘s latest article, during a red teaming engagement in 2024 we found ourselves dealing with a Java applet! We also discovered that most of our young colleagues (millennials 🥶) don’t even know what a Java applet is… 😔

After having looked a bit at the basic functioning of the applet, we quickly realized that communication was happening through serialized objects, and so we started to dig deeper in order to intercept and modify such objects. At that point, Federico and I took two completely different paths. Of course, the winner was whoever arrived first to the solution!

Federico pursued the path of using an old tool of his to deserialize the objects (with some roadblocks along the way that he had to overcome), while I tried a different approach that I found more congenial to my style… using Frida to instrument the code and call Java classes arbitrarily.

Let’s proceed step by step. Before moving on to applets, let’s start with using Frida on a standard Java application.

Frida Java Bridge on Windows

Of course, all that glitters is not gold… First, I tried to make Frida work on Windows to instrument a Java application, but without success. There are various threads open on this discussion, the main ones being:

As you can see from the source code, Frida searches for a series of functions via DebugSymbol. If these are not present, then the Java bridge does not work correctly.

It seems that DebugSymbol support for Windows is not yet fully functional, so even if we have the PDB files of the Java executables and the JVM, the exported name format is completely different and the Java bridge does not support it.

First dead end. 😵

Frida Java Bridge on Linux with Java 8

At this point, I switched to Linux (Ubuntu 22.04), which seems to be better supported. However, I say “seems” because the Frida Java bridge is a bit of a nightmare for many people. After some research, I realized that to make Frida work correctly it is necessary to have a release of Java that has been compiled with debug symbols. I found these releases that should work correctly:

In particular, I tried these Java versions:

We can check if the necessary symbols for Frida are present in the JVM using this command:

As described by Federico, to minimize compatibility issues during analysis of a old application, I started with the JDK of Java 8… which unfortunately does not seem to contain all the necessary functions to make the Frida Java bridge work.

Second dead end. ☠️

Frida Java Bridge on Linux with Java 11

At this point, I switched to Java 11. Frida started correctly without the previous error, but…

Source: https://cr.openjdk.org/~prr/oco/2018/JDK11Migration.pdf

So even if I managed to make Frida work on this Java version, I would still be unable to instrument the target applet.

However, while searching online, I managed to find the IcedTea-Web project, which should help partially solve the problem. Essentially, it allows to create a JNLP file with settings taken from the Java Web Start page and launch the applet locally. As an example, I used the following JNLP file to run the target applet (the “applet-desc” tag and its parameters are specific of the target applet and should be extracted from the HTML page that runs the applet):

<jnlp spec="1.5+" xmlns="http://www.w3.org/1999/xhtml" codebase="https://#TARGET_URL#" href="file:///#PATH_OF_THIS_JNLP_FILE#.jnlp">
  <information>
    <title>Target Application</title>
    <vendor>some vendor</vendor>
    <description>some Description</description>
    <homepage href="https://#TARGET_URL#"></homepage>
    <offline-allowed/>
  </information>
  <security>
    <all-permissions/>
  </security>
  <resources>
    <j2se version="1.3+"></j2se>
    <jar href="https://#TARGET_URL#/#APPLET_JAR_1#.jar" download="eager" main=true></jar>
    <jar href="https://#TARGET_URL#/#APPLET_JAR_2#.jar" download="eager"></jar>
  </resources>
  <applet-desc name="#APPLICATION_NAME#" 
        main-class="#COM.APPLET.MAINCLASS#" 
        width="1000" 
        height="800"
        align="baseline">
    <param NAME="CODEBASE"   VALUE="https://#TARGET_URL#" >    
    <param name="code" value="#CLASS#.class">
  </applet-desc>
</jnlp>

More details can be found on the project’s home on GitHub:

After setting the necessary JAVA_HOME environment variable, I launched Iced Tea Web and configured it to use the chosen Java 11 JDK. The configuration is done through the itweb-settings binary, as follows.

export JAVA_HOME=/home/test/jdk-11.0.22+7/
./itweb-settings

Then I launched the JNLP pasted above, created from the content of the web page that runs the applet, but I got the following error related to unsigned JARs:

test@test-vmwarevirtualplatform:~/icedtea-web-image/bin$ ./javaws /home/test/a.jnlp

While searching online, I found various potential solutions to this problem (enabling weak algorithms, etc.), but nothing worked.

At this point, I downloaded all the JAR files used by the application and signed them with a self-generated key:

test@test-vmwarevirtualplatform:~$ /home/test/jdk-11.0.22+7/bin/keytool -genkey -alias pluto
test@test-vmwarevirtualplatform:~$ /home/test/jdk-11.0.22+7/bin/jarsigner aaaaaa.jar pluto

Then, I modified the JNLP to read them directly from disk instead of downloading them.

<jnlp spec="1.5+" xmlns="http://www.w3.org/1999/xhtml" codebase="https://#TARGET_URL#" href="file:///#PATH_OF_THIS_JNLP_FILE#.jnlp">
  <information>
    <title>Target Application</title>
    <vendor>some vendor</vendor>
    <description>some Description</description>
    <homepage href="https://#TARGET_URL#"></homepage>
    <offline-allowed/>
  </information>
  <security>
    <all-permissions/>
  </security>
  <resources>
    <j2se version="1.3+"></j2se>
    <jar href="file:///home/test/j/#APPLET_JAR_1#.jar" download="eager" main=true></jar>
    <jar href="file:///home/test/j/#APPLET_JAR_2#.jar" download="eager"></jar>
  </resources>
  <applet-desc name="#APPLICATION_NAME#" 
        main-class="#COM.APPLET.MAINCLASS#" 
        width="1000" 
        height="800"
        align="baseline">
    <param NAME="CODEBASE"   VALUE="https://#TARGET_URL#" >    
    <param name="code" value="#CLASS#.class">
  </applet-desc>
</jnlp>

I relaunched it again and got another error:

A “class not found” exception for a Windows swing (GUI stuff) class in a Linux environment? Quite strange…

While searching for a similar error, I came across the release notes of Java 9.

It’s plausible that those classes have undergone changes, and the specific ones related to Windows may no longer be present in the Linux binaries at all (but maybe they were present in the past). Probably somewhere in the code, also Windows swing classes were instantiated, but never used because we are on Linux. So, to overcome the problem, I tried to create a small package with the missing classes (empty, with only the same class signatures), in order to verify my assumption that those classes were never used (thanks to Federico for the package!). The package can be downloaded from my GitHub repository (https://github.com/inode-/Java_LinuxMissingClasses/). An example of an empty Windows class is the following:

package com.sun.java.swing.plaf.windows;

import javax.swing.plaf.basic.BasicLookAndFeel;

public class WindowsLookAndFeel extends BasicLookAndFeel {

    static final Object HI_RES_DISABLED_ICON_CLIENT_KEY = null;

    public WindowsLookAndFeel() {

    }

    @Override
    public String getName() {
        return null;
    }

    @Override
    public String getID() {
        return null;
    }

    @Override
    public String getDescription() {
        return null;
    }

    @Override
    public boolean isNativeLookAndFeel() {
        return true;
    }

    @Override
    public boolean isSupportedLookAndFeel() {
        return true;
    }
}

I compiled this package:

I signed it and added it to the JNLP:

<jnlp spec="1.5+" xmlns="http://www.w3.org/1999/xhtml" codebase="https://#TARGET_URL#" href="file:///#PATH_OF_THIS_JNLP_FILE#.jnlp">
  <information>
    <title>Target Application</title>
    <vendor>some vendor</vendor>
    <description>some Description</description>
    <homepage href="https://#TARGET_URL#"></homepage>
    <offline-allowed/>
  </information>
  <security>
    <all-permissions/>
  </security>
  <resources>
    <j2se version="1.3+"></j2se>
    <jar href="file:///home/test/j/#APPLET_JAR_1#.jar" download="eager" main=true></jar>
    <jar href="file:///home/test/j/#APPLET_JAR_2#.jar" download="eager"></jar>
    <jar href="file:///home/test/j/PackageWithWindowsEmptySwingClasses.jar" download="eager"></jar>
  </resources>
  <applet-desc name="#APPLICATION_NAME#" 
        main-class="#COM.APPLET.MAINCLASS#" 
        width="1000" 
        height="800"
        align="baseline">
    <param NAME="CODEBASE"   VALUE="https://#TARGET_URL#" >    
    <param name="code" value="#CLASS#.class">
  </applet-desc>
</jnlp>

And finally, Frida is working! 🎉

Frida Java Bridge on Linux with Java 17

Out of curiosity, let’s now try on Java 17… Same procedure, but the applet did not start correctly. Let’s try enabling debug mode to understand why.

Another error…

Newer versions of Java seem to have further restricted direct access to certain classes (which theoretically should never have been accessed in that way, like the Windows classes replaced above). Fortunately, after some research, I came across an helpful article:

From what I could understand, those classes were directly exposed by Java; however, in theory, they were not supposed to be executed directly, but wrappers were meant to be used. Starting from version 9 and onwards, direct access to those APIs has been restricted, causing some applications to break. To avoid issues of this kind, programmers have released special options that allow making them accessible again, although not by default.

I tried adding these options in IcedTea, but without success. At that point, I checked the complete Java command that IcedTea uses to make the applet work and I manually executed the same command, adding specific options to make those classes usable again.

--add-exports java.desktop/com.sun.java.swing.plaf.motif=ALL-UNNAMED

During some attempts, I also encountered the following error:

After some tests, I noticed an issue related to the SecurityManager. Theoretically, in the latest versions of Java applets, a SecurityManager should always be implemented. However, our applet did not have it implemented.

To solve this problem, I found the option –nosecurity to add at the start of javaws or alternatively -nosecurity just before the JNLP file in the Java command line. I found this suggestion here:

And everything started working correctly even on Java 17! 🎊

Frida Java Bridge with all Applet Classes

At this point, I tried to perform a Java.use frida function on a class of the applet:

I had already encountered a similar issue on Android in the past, often caused by the fact that Java.use only uses the default Java class loader, but we can have different class loaders on our target Java environment that load classes.

By finding the correct class loader, it worked correctly, and I could finally access all the applet’s classes and perform the necessary hooking and tampering:

function javaUseOnAllClassLoaders(className) {

    var classTofind = null;

    try  {
        classToFind = Java.use(className);
        return classToFind;
    } catch(error) {
        console.log("Class not found in current classLoader. Searching in all the classloaders");
        var found = false;
        var classToFind = null;
        var aaa = Java.enumerateClassLoaders({
            onMatch: function(loader){
                if(!found) {
                    Java.classFactory.loader = loader;
                    // Hook the class if found, else try next classloader.
                    try{
                        classToFind = Java.use(className);
                        console.log("*** " + className + " LOADED")
                        found = true;


                    }catch(error){
                        if(error.message.includes("ClassNotFoundException")){
                            console.log("Class not found, trying next loader");
                        } else {
                            console.log(error)
                        }
                    }
                }
            },
            onComplete: function(){
            }
        });
    }
    return classToFind;
}

Java.perform(function() {
    var classToSearchOnAllClassLoaders = javaUseOnAllClassLoaders("com.pluto.paperino");

    if(classToSearchOnAllClassLoaders != null) {
      // Do things
      console.log(classToSearchOnAllClassLoaders)
  }
});

Similarly, the same problem exists with Java.choose (search class instances in the heap), which can be resolved in a similar way:

var found_classloader = null;

function javaUseOnAllClassLoaders(className) {

    var classTofind = null;

    try {
        classToFind = Java.use(className);
        return classToFind;

    } catch (error) {

        console.log("Class not found in current classLoader. Searching in all the classloaders");
        var found = false;
        var classToFind = null;
        var aaa = Java.enumerateClassLoaders({
            onMatch: function (loader) {
                if (!found) {
                    Java.classFactory.loader = loader;

                    // Hook the class if found, else try next classloader.

                    try {
                        classToFind = Java.use(className);
                        console.log("*** " + className + " LOADED")
                        found = true;
                        found_classloader = loader;

                    } catch (error) {
                        if (error.message.includes("ClassNotFoundException")) {
                            console.log("Class not found, trying next loader");
                        } else {
                            console.log(error)
                        }
                    }
                }
            },
            onComplete: function () {

            }

        });
    }
    return classToFind;

}


Java.perform(function () {

    var classToSearchOnAllClassLoaders = javaUseOnAllClassLoaders("com.example.a");

    if (classToSearchOnAllClassLoaders != null) {

        Java.ClassFactory.get(found_classloader).choose("com.example.a.b.c", {
            onMatch: function (instance) {
                if (instance != "") {
                    console.log("INSTANCE: " + instance);

                    // Do things  

                    // Call functions of the found object
                    instance.function_a("3344555");
                    
                    // Create instances of other classes
                    var classe_a = javaUseOnAllClassLoaders("com.example.a.f.g");
                    var instance2 = classe_a.$new(instance);
                    instance2.function_b();

                    // And so on...                    
                }
            },
            onComplete: function () { }
        });
    }
});

Happy hacking in 2024 on Java applications and applets! 🏴‍☠️