Log4j2的JNDI注入漏洞(CVE-2021-44228)
什么是Log4j2?
Log4j2是一个Java日志组件,被各类Java框架广泛地使用。它的前身是Log4j,Log4j2重新构建和设计了框架,可以认为两者是完全独立的两个日志组件。本次漏洞影响范围为Log4j2最早期的版本2.0-beta9到2.15.0。
因为存在前身Log4j,而且都是Apache下的项目,不管是jar包名称还是package名称,看起来都很相似,导致有些人分不清自己用的是Log4j还是Log4j2。这里给出几个辨别方法:
- Log4j2分为2个jar包,一个是接口
log4j-api-${版本号}.jar
,一个是具体实现log4j-core-${版本号}.jar
。Log4j只有一个jar包log4j-${版本号}.jar
。 - Log4j2的版本号目前均为2.x。Log4j的版本号均为1.x。
- Log4j2的package名称前缀为
org.apache.logging.log4j
。Log4j的package名称前缀为org.apache.log4j
。
Log4j2 Lookup
Log4j2的Lookup主要功能是通过引用一些变量,往日志中添加动态的值。这些变量可以是外部环境变量,也可以是MDC中的变量,还可以是日志上下文数据等。
下面是一个简单的Java Lookup例子和输出:
1 | package org.example; |
需要在resources文件夹下添加一个log4j2.xml
1 |
|
输出如下:
1 | 2023-08-10 15:39:58.959 [main] ERROR org.example.Log4j2Lookup - userId: test |
从上面的例子可以看到,通过在日志字符串中加入”${ctx:userId}”,Log4j2在输出日志时,会自动在Log4j2的ThreadContext
中查找并引用userId
变量。格式类似”${type:var}”,即可以实现对变量var的引用。type可以是如下值:
- ctx:允许程序将数据存储在 Log4j
ThreadContext
Map 中,然后在日志输出过程中,查找其中的值。 - env:允许系统在全局文件(如 /etc/profile)或应用程序的启动脚本中配置环境变量,然后在日志输出过程中,查找这些变量。例如:
${env:USER}
。 - java:允许查找Java环境配置信息。例如:
${java:version}
。 - jndi:允许通过 JNDI 检索变量。
- ……
其中和本次漏洞相关的便是jndi,例如:${jndi:rmi//127.0.0.1:1099/a}
,表示通过JNDI Lookup功能,获取rmi//127.0.0.1:1099/a
上的变量内容。
漏洞复现
1 | package org.example; |
rmi服务
1 | package org.example; |
1 | public class Exploit { |
在Exploit.class目录下开启http服务
漏洞原理
由于是JNDI注入,因此可以通过在InitialContext.lookup(String name)方法上设置断点,观察整个漏洞触发的调用堆栈,来了解原理。调用堆栈如下:
由于比较多,并且前面较为单调,所以我们只分析几个关键的地方
首先由LOGGER.error方法最终会调用到MessagePatternConverter的format方法。
该方法对日志内容进行解析和格式化,并返回最终格式化后的日志内容。当碰到日志内容中包含${
子串时,调用StrSubstitutor的replace方法进行进一步解析,跟进
继续调用substitute方法
继续调用一个重载方法,然后到这里
对${}中的内容进行了提取,并赋值给了varName参数,然后调用了resolveVariable方法,跟进
调用了Interpolator的lookup方法
根据前缀,也就是jndi找到相应的lookup类,然后调用其lookup方法,跟进
继续调用jndiManager.lookup方法
调用InitialContext.lookup方法,这之后就和正常的JNDI注入一样了