One reason Log4Shell was so hard to filter is that the malicious string is not fixed. Log4j's own lookup syntax gave attackers a small expression language they could use to rebuild the JNDI token character by character, dodging any signature that matched the literal text. For defenders, understanding these obfuscation techniques is essential: you cannot write a resilient detection if you only know what the plain payload looks like. This article explains the evasion techniques so you can recognise them. It shows what hostile strings look like for detection purposes only and contains no working exploit chain.

Before diving in, confirm where you are exposed by running the Log4Shell scanner against your services.

Why Obfuscation Works Against Naive Filters

The earliest defensive reaction was to block the literal token, for instance a web application firewall rule matching the text jndi. The trouble is that Log4j expands nested lookups before it resolves the JNDI request. So an attacker never has to write jndi literally in the request at all; they can write an expression that evaluates to those letters at runtime. By the time Log4j assembles the real token, it is past the point where your filter inspected the raw input. This is why static string blocking failed so quickly and why removing the vulnerable code path is the only real fix, as covered in mitigation without upgrading.

Technique 1: Case-Folding Lookups

Log4j offered lower and upper lookups that change the case of their argument. Attackers used them to spell out the JNDI keyword indirectly. For example, a fragment like ${lower:J}${lower:N}${lower:D}${lower:I} resolves to the lowercase letters of the keyword without ever containing the contiguous word in the raw request. A signature looking for the plain word sees only a scatter of lower lookups.

For detection, the takeaway is that repeated ${lower: or ${upper: fragments inside a single field are highly abnormal in legitimate traffic and make an excellent alerting signal. Legitimate applications almost never emit clusters of case-folding lookups inside externally supplied fields, so the false-positive rate for this signal is low and the effort to deploy it is small.

Technique 2: Default-Value Tricks

Log4j's lookup syntax supports a default value with the :- separator: if the named key resolves to nothing, the text after :- is used instead. Attackers exploited this with constructs such as ${::-j} or ${what:ever:-j}, where the lookup deliberately resolves to nothing and the default emits a single literal character. Chaining many of these reconstructs the whole token one letter at a time.

From a hunting perspective, a dense cluster of :- separators packed into a short field, especially interleaved with ${ sequences, is a strong indicator of payload reassembly rather than ordinary input. The more of these separators you see crammed into a single short value, the more confident you can be that the field is carrying a reconstructed token rather than anything a normal user would submit.

Technique 3: Environment and Property Indirection

Other lookups could read environment variables or system properties. By referencing a variable that does not exist and supplying a default, or by stitching together values, attackers added another layer of indirection that further fragmented the keyword. The defensive signal is the appearance of ${env:, ${sys:, or similar lookup prefixes inside externally supplied fields, which should almost never happen in normal application traffic.

Technique 4: Encoding on the Wire

On top of Log4j's own expression tricks, the request itself was frequently encoded so that your inspection layer saw something different from what the application ultimately processed. Common wire-level transforms included:

  • URL percent-encoding of the braces, colons, and letters, so the raw HTTP log shows %24%7b instead of ${.
  • Double encoding, where the payload is percent-encoded twice to defeat a single decode pass.
  • Mixed and partial encoding, encoding only a few characters to break a signature while keeping the string functional.
  • Whitespace and unusual separators inserted to disrupt pattern matching.

The defensive lesson is that you must normalise before you match: decode percent-encoding, handle double encoding, and only then run your detection patterns. Searching raw, un-decoded logs will miss a large share of attempts, as discussed in detecting Log4Shell in logs.

Combining the Techniques

Real-world payloads layered these tricks together: a case-folded, default-value-reassembled token, wrapped in URL encoding, placed inside a header. The result bears little visual resemblance to the textbook example, which is exactly the point. No single literal signature can catch them all, so defenders need a strategy built around behaviour and structure rather than exact text.

Building Resilient Detections

Given the variety, design your detections in layers and accept that you are looking for structural anomalies, not one fixed string:

  1. Normalise input first. URL-decode, handle double encoding, and strip noise before matching.
  2. Match the lookup skeleton. Flag any ${ followed eventually by a closing brace inside fields that should never contain Log4j expressions.
  3. Flag obfuscation primitives. Alert on lower, upper, env, sys, and the :- separator appearing in external input.
  4. Count density. A short field containing many ${ or :- fragments is suspicious regardless of the exact letters.
  5. Correlate with network egress. Pair any match with outbound LDAP, RMI, or unusual DNS to confirm a real attempt.

Why You Still Must Patch

Strong detection is valuable, but it is not a substitute for removing the vulnerable code. Because obfuscation is open-ended, there will always be a variant your filter has not seen. Treat detection as visibility and a speed bump, and treat patching to 2.17.1 or the appropriate backport as the actual fix. Confirm your versions with the version checker and follow the patching guide to close the path for good.

Turning Knowledge Into Standing Coverage

Once you understand the obfuscation toolkit, fold it into permanent detection content. A rule that alerts on Log4j lookup syntax in user-controlled fields, tuned with the density and primitive checks above, will keep catching variants long after the initial wave. The same mindset, normalise then match structure, applies to future expression-language and template-injection flaws, so the effort pays dividends well beyond Log4Shell itself.

Testing Your Detections Safely

Once you have built detections, you will want to confirm they actually fire, and you can do this without ever running a real exploit. The safe approach is to feed your detection pipeline benign sample strings that contain the obfuscation structures but point at harmless, internal, non-resolving destinations, then verify that your alerts trigger. Because you are only testing whether your log search and alerting recognise the pattern, there is no need to set up any attack infrastructure or cause any outbound resolution. This lets you tune sensitivity and reduce false positives in a controlled way. Keep these test strings in a documented, access-controlled location so colleagues understand they are detection fixtures rather than live payloads, and so the alerts they generate are recognised as deliberate tests.

Conclusion

Log4Shell payloads are obfuscated using Log4j's own lookup language, case-folding, default-value tricks, environment indirection, and wire-level encoding, layered in combination. Defenders beat this by normalising input and matching structural anomalies rather than literal text, then confirming with network evidence. Map your exposure with the Log4Shell scanner at log4shell.tools and build detections that survive the next variant.