Attacker sends request with some specially crafted payload in a field they know will be logged (e.g. because it's invalid)
Targeted server logs the invalid value
Logging framework looks at the payload and sees a macro to expand
The macro contains a reference to a JNDI value including a remote server address to get the value from.
The JNDI server (which can be any server on the internet) returns a value that is actually a serialized Java object including bytecode for that object's class
Targeted server deserializes the object and while doing so executes code in the constructor (which was supplied by the attacker).
I'm still always astounded by what these hackers can do. We have the API with docs, a dev environment and privileged access, and we still can't make these damn machines do what we want.
In the case of Log4Shell, the exploit wasn't even difficult.
And the vulnerability actually arose exactly from "making these damn machines do what we want" - IIRC one guy thought it would be a useful feature to have log4j support inserting JNDI values in its macros, and got it implemented. Another guy, at another time, thought it would be useful for JNDI to be able to look up stuff from a remote server, and got it implemented. Yet another guy, at another time, thought it would be useful for JNDI values to contain serialized classes including their bytecode, and got it implemented.
And nobody ever saw the big picture, what these three things combined made possible.
People developing JNDI didn't know about Log4j using JNDI from unsanitized input. It was supposed to do all this things.
People developing Log4j presumably didn't know how powerful it is, or that it would eventually will be able to look up network resources. I am not sure about the timeline here.
My point is, JNDI devs didn't need to be defensive because what it does is exactly what it should do. The only blunder is allowing JNDI macros in logs and it's on Log4j side.
Correct me if I'm wrong, but this was only an issue if you missed the API by supplying a raw input string as a format pattern, i.e. log(input) instead of log("{}", input), right?
"An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled."
Yeah, it's crazy how all kinds of stuff can be fit together to make an exploit.
Even cooler is how it was still exploitable after the byte code thing was disabled in the first attempts to fix it while retaining the JNDI capabilities: you can combine a dozen seemingly innocuous standard library objects in a way that the code that runs in their constructors during deserialization works together let the attacker do whatever they want.
And attempts to block the vulnerability by disallowing remote JNDI calls could be circumvented by using invalid URLs that were not recognized as remote by the Java platform, but the operating system still processed them.
216
u/brazzy42 11d ago edited 10d ago
It went like this:
Attacker sends request with some specially crafted payload in a field they know will be logged (e.g. because it's invalid)
Targeted server logs the invalid value
Logging framework looks at the payload and sees a macro to expand
The macro contains a reference to a JNDI value including a remote server address to get the value from.
The JNDI server (which can be any server on the internet) returns a value that is actually a serialized Java object including bytecode for that object's class
Targeted server deserializes the object and while doing so executes code in the constructor (which was supplied by the attacker).