tl;dr Run add our new tool, -javaagent:log4j-jndi-be-gone-1.0.0-standalone.jar
to all of your JVM Java stuff to stop log4j from loading classes remotely over LDAP. This will prevent malicious inputs from triggering the “Log4Shell” vulnerability and gaining remote code execution on your systems.
In this post, we first offer some context on the vulnerability, the released fixes (and their shortcomings), and finally our mitigation (or you can skip directly to our mitigation tool here).
Hello internet, it’s been a rough week. As you have probably learned, basically every Java app in the world uses a library called “log4j” to handle logging, and that any string passed into those logging calls will evaluate magic ${jndi:ldap://...}
sequences to remotely load (malicious) Java class files over the internet (CVE-2021-44228, “Log4Shell”). Right now, while the SREs are trying to apply the not-quite-a-fix official fix and/or implement egress filtering without knocking their employers off the internet, most people are either blaming log4j for even having this JNDI stuff in the first place and/or blaming the issue on a lack of support for the project that would have helped to prevent such a dangerous behavior from being so accessible. In reality, the JNDI stuff is regrettably more of an “enterprise” feature than one that developers would just randomly put in if left to their own devices. Enterprise Java is all about antipatterns that invoke code in roundabout ways to the point of obfuscation, and supporting ever more dynamic ways to integrate weird protocols like RMI to load and invoke remote code dynamically in weird ways. Even the log4j format “Interpolator” wraps a bunch of handlers, including the JNDI handler, in reflection wrappers. So, if anything, more “(financial) support” for the project would probably just lead to more of these kinds of things happening as demand for one-off formatters for new systems grows among larger users. Welcome to Enterprise Java Land, where they’ve already added log4j variable expansion for Docker and Kubernetes. Alas, the real problem is that log4j 2.x (the version basically everyone uses) is designed in such a way that all string arguments after the main format string for the logging call are also treated as format strings. Basically all log4j calls are equivalent to if the following C:
printf("%s\n", "clobbering some bytes %n");
were implemented as the very unsafe code below:
char *buf;
asprintf(&buf, "%s\n", "clobbering some bytes %n");
printf(buf);
Basically, log4j never got the memo about format string vulnerabilities and now it’s (probably) too late. It was only a matter of time until someone realized they exposed a magic format string directive that led to code execution (and even without the classloading part, it is still a means of leaking expanded variables out through other JNDI-compatible services, like DNS), and I think it may only be a matter of time until another dangerous format string handler gets introduced into log4j. Meanwhile, even without JNDI, if someone has access to your log4j output (wherever you send it), and can cause their input to end up in a log4j call (pretty much a given based on the current havoc playing out) they can systematically dump all sorts of process and system state into it including sensitive application secrets and credentials. Had log4j not implemented their formatting this way, then the JNDI issue would only impact applications that concatenated user input into the format string (a non-zero amount, but much less than 100%).
The main fix is to update to the just released log4j 2.15.0. Prior to that, the official mitigation from the log4j maintainers was:
“In releases >=2.10, this behavior can be mitigated by setting either the system property
Apache Log4jlog4j2.formatMsgNoLookups
or the environment variableLOG4J_FORMAT_MSG_NO_LOOKUPS
totrue
. For releases from 2.0-beta9 to 2.10.0, the mitigation is to remove theJndiLookup
class from the classpath:zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
.”
So to be clear, the fix given for older versions of log4j (2.0-beta9 until 2.10.0) is to find and purge the JNDI handling class from all of your JARs, which are probably all-in-one fat JARs because no one uses classpaths anymore, all to prevent it from being loaded.
Then there is the fix for more recent versions of log4j (2.10.0 to 2.14.1), which is to set a magic Java system property (log4j2.formatMsgNoLookups
) via the command line or weird XML shenanigans, or to add a magic environment variable (LOG4J_FORMAT_MSG_NO_LOOKUPS
) to your Java processes (likely by reconfiguring your [systemd] service definitions). And you should be aware that both of these simply disable all ${}
handling. Lastly, the real fix is to ‘just update it’ to the new version (2.15.0) that defaults to the functionality being turned off in an overly complicated patch that… removes the formatMsgNoLookups
handling, but sets the default to true? They apparently changed the handling so that lookup handlers are opt-in, enabled by XML-based configuration, a change so cumbersome I question how long it will last.
None of these solutions are ideal because they all presume that everyone has a 100% handle on their dependency chains and that you won’t end up with a mix of log4j versions strewn across the subcomponents of apps segmented across multiple classloaders (looking at you, WAR files). The reality is that the node_modules-style of dependency embedding is not completely alien to the Java ecosystem (and Java has a host of left-pad type issues due to how its build systems resolve dependencies across multiple repositories). In addition to this, many apps and proprietary libraries and app servers possibly vendor-in log4j in weird ways that end up preferring their own versions over any shared version that might actually be updated by your ops team. All told, tracking down and updating every instance of a library like this in your application stack is a nontrivial process and potentially error prone as any one rogue log4j copy can potentially keep a service vulnerable.
To try to make the situation a little bit more manageable in the meantime, we are releasing log4j-jndi-be-gone, a dead simple Java agent that disables the log4j JNDI handler outright. log4j-jndi-be-gone uses the Byte Buddy bytecode manipulation library to modify the at-issue log4j class’s method code and short circuit the JNDI interpolation handler. It works by effectively hooking the at-issue JndiLookup
class’ lookup()
method that Log4Shell exploits to load remote code, and forces it to stop early without actually loading the Log4Shell payload URL. It also supports Java 6 through 17, covering older versions of log4j that support Java 6 (2.0-2.3) and 7 (2.4-2.12.1), and works on read-only filesystems (once installed or mounted) such as in read-only containers.
The benefit of this Java agent is that a single command line flag can negate the vulnerability regardless of which version of log4j is in use, so long as it isn’t obfuscated (e.g. with proguard), in which case you may not be in a good position to update it anyway. log4j-jndi-be-gone is not a replacement for the -Dlog4j2.formatMsgNoLookups=true
system property in supported versions, but helps to deal with those older versions that don’t support it.
Using it is pretty simple, just add -javaagent:path/to/log4j-jndi-be-gone-1.0.0-standalone.jar
to your Java commands. In addition to disabling the JNDI handling, it also prints a message indicating that a indicating that a log4j JNDI attempt was made with a simple sanitization applied to the URL string to prevent it from becoming a propagation vector. It also “resolves” any JNDI format strings to "(log4j jndi disabled)"
making the attempts a bit more grep-able.
$ java -javaagent:log4j-jndi-be-gone-1.0.0.jar -jar myapp.jar
log4j-jndi-be-gone is available from our GitHub repo, https://github.com/nccgroup/log4j-jndi-be-gone. You can grab a pre-compiled log4j-jndi-be-gone agent JAR from the releases page, or build one yourself with ./gradlew
, assuming you have a recent version of Java installed.