前言
最近Log4j2的JNDI注入漏洞(CVE-2021-44228)可以称之为“核弹”级别。Log4j2作为类似JDK级别的基础类库,几乎没人能够幸免。极盾科技技术总监对该漏洞进行复现和分析其形成原理。在此分享。
以下涉及的代码,均在mac OS 10.14.5,JDK1.8.0_91环境下成功运行。
一、 前置知识
1.1 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 。
1.2 Log4j2 Lookup
Log4j2的Lookup主要功能是通过引用一些变量,往日志中添加动态的值。这些变量可以是外部环境变量,也可以是MDC中的变量,还可以是日志上下文数据等。
下面是一个简单的Java Lookup例子和输出:
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(Log4j2RCEPoc.class);
public static void main(String[] args) {
ThreadContext.put("userId", "test");
LOGGER.error("userId: ${ctx:userId}");
}
}
10:21:19.618 [main] ERROR Log4j2RCEPoc - userId: test
从上面的例子可以看到,通过在日志字符串中加入"${ctx:userId}",Log4j2在输出日志时,会自动在Log4j2的 ThreadContext 中查找并引用 userId 变量。格式类似"${type:var}",即可以实现对变量var的引用。type可以是如下值:
ThreadContext
${env:USER}
${java:version}
其中和本次漏洞相关的便是jndi,例如: ${
jndi:rmi//127.0.0.1:1099/a} ,表示通过JNDI Lookup功能,获取 rmi//127.0.0.1:1099/a 上的变量内容。
1.3 JNDI
JNDI(Java Naming and Directory Interface,Java命名和目录接口),是Java提供的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象 。
例如使用数据库,需要在各个应用中配置各种数据库相关的参数后使用。通过JNDI,可以将数据库相关的配置在一个支持JNDI服务的容器(通常Tomat等Web容器均支持)中统一完成,并暴露一个简洁的名称,该名称背后绑定着一个 DataSource 对象。各个应用只需要通过该名称和JNDI接口,获取该名称背后的 DataSource 对象。当然,现在SpringBoot单体发布模式,极少会使用这种方式了。
再举个更简单的例子,这有点类似DNS提供域名到IP地址的解析服务。域名简洁易懂,便于普通用户使用,背后真正对应的是一个复杂难记的IP,甚至还可能是多个IP。DNS即JNDI服务,域名即可用于绑定和查找的名称,IP即该名称绑定的真正对象。用现代可以类比的技术来说,JNDI就是一个对象注册中心。
JNDI由三部分组成:JNDI API、Naming Manager、JNDI SPI。JNDI API是应用程序调用的接口,JNDI SPI是具体实现,应用程序需要指定具体实现的SPI。下图是官方对JNDI介绍的架构图:
下面是一个简单的例子:
public interface Hello extends java.rmi.Remote {
public String sayHello(String from) throws java.rmi.RemoteException;
}
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements Hello {
public HelloImpl() throws java.rmi.RemoteException {
super();
}
@Override
public String sayHello(String from) throws java.rmi.RemoteException {