I know that Log4Shell alert is infuriating. I spent the week before Christmas 2021 on calls with panicked DevOps teams because a single ${jndi:ldap://...} string in a User-Agent header owned entire fleets. Nobody had touched these apps in years, and suddenly they were remote code execution servers.
The root cause is almost always a stale log4j-core JAR sitting in your classpath. Version 2.0-beta9 through 2.14.1, and 2.15.0 if you hit the follow-up DoS. Here's how to find it and what to actually do about it.
Cause 1: You're still running log4j-core 2.x (the big one)
This is the answer 9 times out of 10. Some app you forgot about — a Kafka consumer, a Solr node, a half-retired Spring Boot service — ships with log4j-core-2.11.1.jar in its lib/ folder. Attackers scan the whole internet for it. You don't even need to log the request; the JNDI lookup fires during string interpolation.
Find every copy on disk right now:
find / -name 'log4j-core*.jar' -type f 2>/dev/null | while read f; do
echo "$f"
unzip -p "$f" META-INF/MANIFEST.MF | grep -i version
done
On Windows, PowerShell does the trick:
Get-ChildItem -Path C:\ -Recurse -Filter 'log4j-core*.jar' -ErrorAction SilentlyContinue |
ForEach-Object { $_.FullName }
Also check inside WAR and EAR files — that's where people miss it. An old Jenkins plugin, a Grails app, a Spring Boot fat JAR. If it's nested, your find won't catch it:
find /opt -name '*.war' -o -name '*.ear' | while read w; do
unzip -l "$w" | grep -i 'log4j-core' && echo "HIT: $w"
done
The real fix is upgrade, not a band-aid. Patch log4j-core to 2.17.1 minimum if you're on Java 8. If you're on Java 7, 2.12.4. Java 6, 2.3.2. Those backports exist because a ton of legacy banking and industrial software still runs JDK 6.
Maven users: check your dependency tree, because transitive deps hide this:
mvn dependency:tree -Dincludes=org.apache.logging.log4j:log4j-core
Then force the version in <dependencyManagement>. Gradle folks: ./gradlew dependencyInsight --dependency log4j-core.
Cause 2: A vendored JAR inside another library
This one trips people up. You upgrade your own pom.xml, redeploy, run a scan, and the scanner still screams. Why? Because elasticsearch-rest-client-7.10.2.jar, or an old AWS SDK, or a Netty transport, shades its own copy of log4j-core. Yes, vendored and relocated sometimes. Sometimes not relocated at all.
The way to catch this is scanning extracted contents, not filenames:
find / -name '*.jar' -type f -exec sh -c '
unzip -l "$1" 2>/dev/null | grep -q "org/apache/logging/log4j/core/lookup/JndiLookup.class" && echo "$1"
' _ {} \;
Any hit means that JAR bundles the vulnerable class. Now you have three paths: force the parent library to a fixed version, use Maven Shade to exclude it, or rip the class out manually.
Ripping it out is a legitimate emergency move. It's ugly. It works. The JNDI lookup class is the entire attack surface for the common case:
zip -q -d log4j-core-2.14.1.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
I've done this on production Kafka brokers at 2 AM. It buys you time. It is not a substitute for patching.
Cause 3: Your scanner is flagging the JndiLookup class after a partial patch
You upgraded to 2.15.0, so you're safe, right? No. 2.15.0 fixed the LDAP RCE but was still vulnerable to a DoS (CVE-2021-45046) — attackers could crash the JVM by sending a crafted payload that caused a stack overflow in the message lookup. Same weekend, new panic.
Also, some scanners just grep for JndiLookup.class in every JAR on disk. If you're on 2.17.x, that class still exists — it's disabled by default, not removed. Your scanner will light up red and your security team will ping you on Slack. Don't panic.
Confirm the actual behavior. Log4j 2.16+ disabled JNDI by default. 2.17.x removed message lookups from LDAP entirely. Verify with:
unzip -p /path/to/log4j-core-2.17.1.jar META-INF/MANIFEST.MF | grep -i version
# Expect: Implementation-Version: 2.17.1
And check your config. Someone may have explicitly re-enabled JNDI with this system property:
-Dlog4j2.enableJndiLookup=true
If that's in your JAVA_OPTS, remove it. Same with log4j2.enableJndiJms and log4j2.enableJndiContextSelector — those were the escape hatches people added in December 2021 and never cleaned up.
Quick mitigation if you can't patch today
Set this system property to force the lookup off:
-Dlog4j2.formatMsgNoLookups=true
It works on 2.10 through 2.14.1. Doesn't help on 2.0-beta9 to 2.9 — those need the JAR upgrade or class removal. Add it to every JVM that touches log4j, including your CI runners, your IDEs, and that forgotten Bamboo agent from 2019.
Summary table
| Scenario | Symptom | Fix |
|---|---|---|
Direct log4j-core 2.0–2.14.1 | RCE via ${jndi:ldap://} in any logged header, param, or body | Upgrade to 2.17.1 (Java 8), 2.12.4 (Java 7), 2.3.2 (Java 6) |
| Vendored JAR inside another library | Scanner still flags after your own pom is patched | Force parent dep upgrade, or zip -d the JndiLookup.class |
| Post-2.15 scanner false positive | Alert persists though you already upgraded | Verify manifest version; remove enableJndiLookup=true system property |
| Can't patch right now | App must keep running | -Dlog4j2.formatMsgNoLookups=true plus egress firewall block on LDAP/RMI |
One more thing: block outbound LDAP and RMI at your firewall. CVE-2021-44228 is three years old and I still see it during pentests. Somebody always has a 2.13.3 JAR hiding in a Docker image layer. Scan the image, not just the running container.