Arpan Bandyopadhyay
Posted on December 19, 2021
What is Log4j?
log4j is a logging framework written in Java which is distributed under the Apache Software License.
Log4j2 is an updated version of Log4j.
Description of the vulnerability:
Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.15.0, this behavior has been disabled by default. From version 2.16.0, this functionality has been completely removed. Note that this vulnerability is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging Services projects.
The usage of the nasty vulnerability in the Java logging library Apache Log4j that allowed unauthenticated remote code execution.
What is RCE?
It stands for Remote code execution. It allows hacker to run any code in your application by hacking your application which is using log4j. This vulnerability nicknamed as Log4Shell.
What is Log4Shell vulnerability?
Log4Shell is a software vulnerability in Apache Log4j 2, a popular Java library for logging error messages in applications. The vulnerability, published as CVE-2021-44228, enables a remote attacker to take control of a device on the internet, if the device is running certain versions of Log4j 2.
There are multiple things that have happened all together resulted in this vulnerability
- Log4j log expression: Log4j allows you to log expression.
Here is the example :
private static final Logger logger = LogManager.getLogger(TestController.class);
logger.error("error message: {}",exception.getMessage());
Here first we are getting Logger object then in 2nd line , exception message from exception.getMessage() will be plugged in to “error message” inside {}- curly braces .
- JNDI: It stands for Java Naming Directory Interface. JNDI allows you to store your java objects in a remote location and streaming them to your JVM.
Here is one example:
This is an sample LDAP URL :
ldap://192.168.3.2:8034/user=Arpan,city=bangalore,country=india
I can invoke this URL I will get serialized profile of Arpan from active directory.
This is not Log4j . This is Java feature.
- JNDI Look up in log message: In 2013 JNDI lookup was introduced in log4j.
The JNDI look up allows variables to be retrieved via JNDI.
Good use case is centralized log configuration.
Here Log4j is going to look up the Value (Getting the prefix for logging message for my log message from JNDI by passing JNDI url as an argument) and inserted the value in the {}- curly braces .
This is the vulnerability:
How?
Lets say I have one search API which takes input from User and search it at server and returns response .
http://localhost:8080/search?data=
If we pass ${jndi:ldap://127.0.0.1:3089/} as a request parameter for data here , then log4j will make a JNDI call to it. This is a problem.
Here is a simple diagram which explains the vulnerability
Lets assume Hacker sets up one malicious JNDI server and sends a request which contains JNDI url for which end point is a malicious java class present in malicious JNDI server . Now Log4j will look up the url and makes an JNDI request to the JNDI server and JNDI server returns back a serialized malicious object which contains malicious content which can destroy and exploit the application. This is how hackers can have ability to put malicious code in the application's JVM .
This is what the vulnerability is.
Here I have prepared one demo to show the vulnerability .
To do this I have created one spring boot project , in which I have used log4j 2.14 dependency which is vulnerable and I have created one very simple API which returns String .
@RestController
public class TestController {
private static final Logger logger = LogManager.getLogger(TestController.class);
@GetMapping("/search")
public String search(@RequestParam("data") String data) {
logger.info("user input to search data : " + data);
return data;
}
}
Now I will start my spring boot server and hit the server with data as "google". As a result it will return "google". Nothing issue in that.
In server logs also quite clean. No issue.
2021-12-20 00:58:36.307 INFO 17028 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-12-20 00:58:36.307 INFO 17028 --- [nio-8080-exec-1] o.s.w.s.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2021-12-20 00:58:36.309 INFO 17028 --- [nio-8080-exec-1] o.s.w.s.DispatcherServlet : Completed initialization in 1 ms
2021-12-20 00:58:36.402 INFO 17028 --- [nio-8080-exec-1] c.l.v.l.c.TestController : user input to search data : google
Now I am going to send JNDI url in data as request param .
Lets see what will happen
(Before sending the JNDI url , I have encoded the url)
From service response I have received whatever I have sent that's fine . But let's look in to the server log
2021-12-20 01:01:35,258 http-nio-8080-exec-3 WARN Error looking up JNDI resource [ldap://127.0.0.1:3089/]. javax.naming.CommunicationException: 127.0.0.1:3089 [Root exception is java.net.ConnectException: Connection refused: connect]
at com.sun.jndi.ldap.Connection.<init>(Connection.java:238)
at com.sun.jndi.ldap.LdapClient.<init>(LdapClient.java:137)
at com.sun.jndi.ldap.LdapClient.getInstance(LdapClient.java:1609)
at com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2749)
at com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:319)
at com.sun.jndi.url.ldap.ldapURLContextFactory.getUsingURLIgnoreRootDN(ldapURLContextFactory.java:60)
at com.sun.jndi.url.ldap.ldapURLContext.getRootURLContext(ldapURLContext.java:61)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:202)
at com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at org.apache.logging.log4j.core.net.JndiManager.lookup(JndiManager.java:172)
at org.apache.logging.log4j.core.lookup.JndiLookup.lookup(JndiLookup.java:56)
at org.apache.logging.log4j.core.lookup.Interpolator.lookup(Interpolator.java:221)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.resolveVariable(StrSubstitutor.java:1110)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:1033)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:912)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.replace(StrSubstitutor.java:467)
at org.apache.logging.log4j.core.pattern.MessagePatternConverter.format(MessagePatternConverter.java:132)
at org.apache.logging.log4j.core.pattern.PatternFormatter.format(PatternFormatter.java:38)
at org.apache.logging.log4j.core.layout.PatternLayout$PatternSerializer.toSerializable(PatternLayout.java:344)
at org.apache.logging.log4j.core.layout.PatternLayout.toText(PatternLayout.java:244)
at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:229)
at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:59)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.directEncodeEvent(AbstractOutputStreamAppender.java:197)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.tryAppend(AbstractOutputStreamAppender.java:190)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append(AbstractOutputStreamAppender.java:181)
at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156)
at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:129)
at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:120)
at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84)
at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:540)
at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:498)
at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:481)
at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:456)
at org.apache.logging.log4j.core.config.AwaitCompletionReliabilityStrategy.log(AwaitCompletionReliabilityStrategy.java:82)
at org.apache.logging.log4j.core.Logger.log(Logger.java:161)
at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2205)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2159)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2142)
at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2017)
at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1983)
at org.apache.logging.log4j.spi.AbstractLogger.info(AbstractLogger.java:1320)
at com.log4j2.vulnerabilities.log4jdemo.controller.TestController.search(TestController.java:15)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:526)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:860)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1589)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.net.ConnectException: Connection refused: connect
at java.net.DualStackPlainSocketImpl.connect0(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at java.net.Socket.connect(Socket.java:538)
at java.net.Socket.<init>(Socket.java:434)
at java.net.Socket.<init>(Socket.java:211)
at com.sun.jndi.ldap.Connection.createSocket(Connection.java:375)
at com.sun.jndi.ldap.Connection.<init>(Connection.java:215)
... 92 more
2021-12-20 01:01:33.090 INFO 17028 --- [nio-8080-exec-3] c.l.v.l.c.TestController : user input to search data : ${jndi:ldap://127.0.0.1:3089/}
Here we can see Log4j is trying to access the JNDI url to get the value to inject it in to the log message. With the same way hacker can send his/her malicious JNDI server details as an input and send their malicious object to the actual running application and can exploit it .
Now question is how to solve this :
- You can update the version of log4j2 to 2.17
How?
To do this just add below property to your build.gradle
ext['log4j2.version'] = '2.17.0'
- You can disable the look up by passing the JVM arguments in the configuration.
How?
Go to Edit configuration and do the following and add VM option and add below
-Dlog4j2.formatMsgNoLookups=true
Click apply & ok. Then Restart the server.
After doing the above changes I will hit the same URL again. Now we can see the logs that JNDI look up does not happen.
2021-12-20 01:12:37.671 INFO 20384 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-12-20 01:12:37.672 INFO 20384 --- [nio-8080-exec-1] o.s.w.s.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2021-12-20 01:12:37.674 INFO 20384 --- [nio-8080-exec-1] o.s.w.s.DispatcherServlet : Completed initialization in 2 ms
2021-12-20 01:12:37.761 INFO 20384 --- [nio-8080-exec-1] c.l.v.l.c.TestController : user input to search data : ${jndi:ldap://127.0.0.1:3089/}
I hope log4j vulnerability is clear to all now .
This is the github link where I kept this code .
https://github.com/ArpanForGeek/log4jdemo.git
Reference: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228
Happy learning
Thanks
Arpan
Let's connect:
LinkedIn : https://www.linkedin.com/in/arpan-bandyopadhyay-bb5b1a54/
Posted on December 19, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.