HowTo: intercept mutually-authenticated TLS communications of a Java thick client

Published on Wed 31 March 2021 by @SAERXCIT

Introduction

Pentesting a thick Java client application can end up being trickier and way more time-consuming than expected, if the client implements security measures such as obfuscation and TLS encryption with mutual authentication, client certificate, and server certificate pinning.

While these security features are not unusual in the mobile space, they are less common in thick client applications. Since a quick Google search did not come up with simple steps to achieve this, I decided to write this small guide on how to quickly set up an intercepting proxy on Java applications implementing these protections, using jdb, in the hope that it will help fellow pentesters save some time in the future.

As I'm not really familiar with Java internals, I may be reinventing the wheel and/or missing obvious stuff, so don't hesitate to point out better ways where you spot them!

First contact

Analyzing the client’s network communications with Wireshark shows whether a client certificate is being used in the TLS handshake.

When TLS client authentication is performed, a good place to start is looking for KeyStore files - where client certificates are typically stored for Java applications - in the application's directory or the jar itself. Often, one KeyStore file is used for the client’s private key and certificate, and another for CAs and servers certificate pinning (when the pinning is not just a hardcoded fingerprint in the code). Let's call these ClientKeyStore and ClientTrustStore respectively.

Note: KeyStores can be of different types. They show up explicitly in the output of a file command, except for PKCS12, which shows up as data in my tests.

KeyStores can be manipulated using the JRE tool keytool, which can transform them into the more familiar PKCS #12 format, if they’re not password protected.

$ keytool -importkeystore -srckeystore ClientKeyStore -destkeystore ClientKeyStore.p12 -deststoretype PKCS12
[...]
Enter key password for <clientkey>
keytool error: java.lang.Exception: Too many failures - try later

$ keytool -importkeystore -srckeystore ClientTrustStore -destkeystore ClientTrustStore.p12 -deststoretype PKCS12
[...]
Entry for alias servercert successfully imported.
Import command completed:  1 entries successfully imported, 0 entries failed or cancelled

$ openssl pkcs12 -in ClientTrustStore.p12
Enter Import Password:
Bag Attributes
    friendlyName: servercert
    2.16.840.1.113894.746875.1.1: <Unsupported tag 6>
subject=C = XXX, ST = XXX, L = XXX, O = Values, OU = Modified, CN = server.com, emailAddress = XXX

issuer=C = XXX, ST = XXX, L = XXX, O = Values, OU = Modified, CN = server.com, emailAddress = XXX

-----BEGIN CERTIFICATE-----
MIIDc[...]
-----END CERTIFICATE-----

The ClientTrustStore certificate can be checked against the server certificate by connecting directly to the server with an openssl s_client command and retrieving the certificate presented. The certificate in the TrustStore is typically either the CA certificate or the server certificate itself.

The next steps will be:

  • Dumping the password from the client to decrypt the private key from the ClientKeyStore.

  • Load a fake TrustStore in place of the legitimate ClientTrustStore to bypass pinning.

Note: if a password is also needed for the ClientTrustStore, the procedure detailed in the next section can also be used.

Obtaining the client’s private key

A straightforward method to dump the password is using jdb.

Starting the client in a suspended state and enabling a debugging interface:

$ java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:8000,server=y,suspend=y -jar TestedClient.jar

Connecting to this debug interface in another terminal:

$ jdb -connect com.sun.jdi.SocketAttach:port=8000

Breakpoints can be set on specific methods with the stop in command. Once a breakpoint triggers, the local context can be accessed and manipulated with locals, set, eval, dump, etc.

Identifying a good place to put breakpoints (especially in the case of an obfuscated client) usually requires reading a bit of documentation. The Java documentation details how and by what other methods and classes java.security.KeyStore objects are used. Interesting methods are ones which take a password as a parameter, as the password will then be in the local context. A similar approach can be used with third-party libraries, either looking at the library documentation if available, or searching from public code samples with the library name and some keywords.

In the Java 8 API, 3 methods fit this criterion:

KeyManagerFactorySpi.engineInit(KeyStore ks, char[] password)
KeyManagerFactory.init(KeyStore ks, char[] password)
KeyStore.Builder.newInstance(KeyStore keyStore, KeyStore.ProtectionParameter protectionParameter)

Note: KeyStore.ProtectionParameter is an interface implemented by KeyStore.PasswordProtection, which is basically a class wrapping a password attribute.

Since breakpoints cannot be set on abstract classes and interfaces, this leaves only KeyManagerFactory.init. For KeyStore.ProtectionParameter, the two constructors of KeyStore.PasswordProtection will be used, as they are the ones taking a password as parameter.

Setting a breakpoint on these and running the application triggers the breakpoint. Then, running locals to find the password variable name and dump <variable_name> shows the cleartext password:

main[1] stop in javax.net.ssl.KeyManagerFactory.init(java.security.KeyStore, char[])
Deferring breakpoint javax.net.ssl.KeyManagerFactory.init(java.security.KeyStore, char[]).
It will be set after the class is loaded.

main[1] stop in java.security.KeyStore.PasswordProtection.<init>(char[])
Deferring breakpoint java.security.KeyStore.PasswordProtection.<init>(char[]).
It will be set after the class is loaded.

main[1] stop in java.security.KeyStore.PasswordProtection.<init>(char[], java.lang.String, java.security.spec.AlgorithmParameterSpec)
Deferring breakpoint java.security.KeyStore.PasswordProtection.<init>(char[], java.lang.String, java.security.spec.AlgorithmParameterSpec).
It will be set after the class is loaded.

main[1] run
> Set deferred breakpoint javax.net.ssl.KeyManagerFactory.init(java.security.KeyStore, char[])

Breakpoint hit: "thread=AWT-EventQueue-0", javax.net.ssl.KeyManagerFactory.init(), line=274 bci=0

AWT-EventQueue-0[1] locals
Method arguments:
ks = instance of java.security.KeyStore(id=3863)
password = instance of char[8] (id=3864)
Local variables:

AWT-EventQueue-0[1] dump password
 password = {
P, 4, 5, 5, w, 0, r, D
}

Notes:

  • jdb requires using full class paths.

  • the .<init> syntax allows breaking in the constructor of a class.

The last commands can be repeated as long as the breakpoints get triggered in order to gather potential passwords to try against the ClientKeyStore (using the keytool command) until the correct password is found. openssl pkcs12 commands can then be used to extract the private key (in client_key_cleaned.pem) and related certificate (in client_cert_cleaned.pem). Testing a connection to the server endpoint with openssl s_client allows validating the credentials and checking whether the encapsulated protocol is HTTP.

Note: For the rest of this guide, we’ll assume a proprietary protocol is used, as intercepting HTTP is more straightforward.

Bypassing the certificate pinning

Bypassing certificate pinning that uses a TrustStore can be done by generating a fake TrustStore with fake server or CA certificate. Mimicking the original ClientTrustStore characteristics as closely as possible will maximize chances of it being accepted by the application without too much fiddling; this includes:

  • Using the same password, or absence thereof.

  • Using the same type (JKS, PKCS12...).

  • Using the same alias to store the certificate.

  • Using the same characteristics as the target certificate: attributes, modulus size, issuer, signature algorithm, issuing date, expiry, x509v3 extensions…

Type and alias can be retrieved using keytool -list -keystore ClientTrustStore, and at this point the password (if any) used on the TrustStore is already known.

The exact steps to generate a private key (dummy_key.pem) and a certificate (dummy_cert.pem) will depend on the nature of the certificate in the ClientTrustStore (CA or server), but the documentation on this is abundant.

If the password is at least 6 characters long, importing dummy_cert.pem into a Java KeyStore can be done directly using keytool:

$ keytool -import -alias <alias> -file dummy_cert.pem -keystore dummy_truststore.jks -storetype <type>

If the password is shorter or no password is used, it is possible to create KeyStores programmatically with Java code. The following code, taken and adapted from this StackOverflow answer, can be used:

import java.io.*;
import java.security.KeyStore;
import java.security.GeneralSecurityException;
import java.security.cert.*;

public class KeyStoreGen {

    private static Certificate readCert(String path) throws IOException, CertificateException {
        try (FileInputStream fin = new FileInputStream(path)) {
            return CertificateFactory.getInstance("X.509").generateCertificate(fin);
        }
    }

    public static void main(String[] args) {
        try {
            // Reading the cert
            Certificate cert = readCert(args[0]);

            // Creating an empty JKS keystore
            KeyStore keystore = KeyStore.getInstance(args[2]);
            keystore.load(null, null);

            // Adding the cert to the keystore
            keystore.setCertificateEntry(args[3], cert);

            FileOutputStream fout = new FileOutputStream(args[1]);
            keystore.store(fout, args[4].toCharArray());
        } catch (GeneralSecurityException | IOException e) {
            e.printStackTrace();
        }
    }
}
$ javac KeyStoreGen.java
$ java KeyStoreGen dummy_cert.pem dummy_truststore.jks <type> <alias> <password>

Intercepting traffic

If the protocol used for network communications is not HTTP, mitm_relay by GitHub user @jrmdev can be used to set up a TLS-intercepting proxy, as it provides enough features for pentesting the thick client:

  • Client TLS authentication.

  • Requests and responses forwarding to a traditional HTTP proxy for dynamic modification of the communications (done by encapsulating them in HTTP).

  • Scripts to automatically perform actions on the requests and responses (e.g. peeking into compressed messages).

Note: several other tools allow intercepting non-HTTP traffic – Canape is a good example, it is recommended to choose what you’re most comfortable with.

Setting up a relay with the dumped client certificate & private key and the generated server/CA certificate & private key with mitm_relay can be done as follows. The -p option will forward requests and responses to a traditional HTTP proxy, for on-the-fly modification:

$ python3 mitm_relay.py -r tcp:12345:endpoint:12345 -cc client_cert_cleaned.pem -ck client_key_cleaned.pem -c dummy_cert.pem -k dummy_key.pem -p http://127.0.0.1:8080
[+] Webserver listening on ('127.0.0.1', 49999)
[+] Relay listening on tcp 12345 -> endpoint:12345

The next step is forcing the Jar client to connect to the relay, instead of to the legitimate server port. If the client does not allow direct configuration of the server endpoint, looking at network traffic in Wireshark will help decide on the method:

  • If a DNS query is sent out before connecting to the endpoint (e.g. hardcoded hostname): override the DNS lookup via an entry in the hosts file pointing to 127.0.0.1 and use the same port number.

  • If the TCP connection happens without a preceding DNS query (e.g. hardcoded IP): use an iptables redirection (the mitm_relay readme gives an example procedure) in order to intercept communications.

The Jar client must be told to use the fake TrustStore for certificate validation. Looking in the Java documentation once again yields two other classes (TrustManagerFactory and TrustManagerFactorySpi) that expose methods taking a KeyStore as parameter, both also named init.

A breakpoint can only be set on TrustManagerFactory.init. Once this breakpoint triggers, the following syntax can be used to update the ClientTrustStore with the fake one (be sure to replace <password> with the actual password):

main[1] stop in javax.net.ssl.TrustManagerFactory.init(java.security.KeyStore)
Deferring breakpoint javax.net.ssl.TrustManagerFactory.init(java.security.KeyStore).
It will be set after the class is loaded.

main[1] run
> Set deferred breakpoint javax.net.ssl.TrustManagerFactory.init(java.security.KeyStore)

Breakpoint hit: "thread=AWT-EventQueue-0", javax.net.ssl.TrustManagerFactory.init(), line=281 bci=0

AWT-EventQueue-0[1] locals
Method arguments:
ks = instance of java.security.KeyStore(id=3660)
Local variables:

AWT-EventQueue-0[1] eval ks.load(new java.io.FileInputStream("dummy_truststore.jks"), "<password>".toCharArray())
 ks.load(new java.io.FileInputStream("dummy_truststore.jks"), "<password>".toCharArray()) = <void value>

Resuming execution shows the decrypted communications appearing in mitm_relay:

mitmrelay intercepting comms

Running mitm_relay with the -p flag makes it relay requests and responses to an HTTP proxy for dynamic modification:

mitmrelay to Burp

And we’re done, we’re now able to dynamically intercept and modify the client’s communications (manually in a proxy tool and/or via scripts), the pentest can begin!

Conclusion

This setup comes with some limitations, some of which are specific to the use of the chosen interception tool:

  • mitm_relay does not allow replaying of the requests, only dynamic interception and modification of the requests sent out by the client

  • If requests and/or responses are encoded in transit (for instance compressed), their modification becomes cumbersome. mitm_relay already allows providing scripts to perform actions on the packets, but only before sending the request to the HTTP proxy, not after. Patching the code to allow post-HTTP-proxy hooks might be necessary in this case.

Additionally, using a debugger to bypass security features implies a lot of manual interventions: for example, the pinning bypass must be performed with jdb every time a TLS handshake is initiated.

However, this relatively simple setup does allow testing the client, which was ultimately the objective.

I hope that this guide will save you some time if you ever get thrown into such a situation. I found this was a good opportunity to learn about TLS stuff in Java. As I’m not very familiar with it I may have missed some things, so if you know of better and/or different ways, please share them!

While looking for references for this guide, I stumbled upon this blog post from Piergiovanni Cipolloni presenting the same certificate pinning bypass technique, but using Frida in the context of an Android application.

Thanks to my teammates for their help and their review of this guide.