基础概念
在开始从代码层面分析漏洞之前,需要先了解两个概念:RMI和JNDI。
RMI:全称为(Remote Method Invocation)远程方法调用,它的主要作用是实现远程对象之间的无缝调用。其设计初衷是为了解决Java的网络分布式应用系统难题。任何基于RMI运行的系统都能够完美移植到任何java虚拟机上。在JNDI注入中,主要用于引入外部编译好的字节码文件。
JNDI:全称(Java Naming and Directory Interface)Java命名和目录接口,它的作用是提供统一的api。通过api来访问相对应的服务接口,可以访问LDAP、RMI、DNS或者其他目录服务。首先,JNDI提供的接口是一种抽象接口,这是为了解决服务访问者的麻烦,通过JNDI提供的高度抽象接口服务访问者就能够使用统一的接口来操作各种不同的命名服务或者目录服务。另一方面,由于服务提供者所提供的服务器接口的多样性,要求JNDI服务需要以一系列接口的形式来接受服务提供者所提供的各种各样的接口。
二者的关系可以简单理解为,JNDI可以获取RMI服务提供的对象,包括远程对象。实际上在漏洞利用这方面,一般都是利用JNDI获取RMI服务提供的远程对象。进行一个类比的话,可以将JNDI理解为一个协议解析服务,该服务可以解析RMI封装协议。具体细节参考官方文档。
漏洞分析
这个漏洞只是从spring框架分析的话,其实危害有限,至少并没有一个很明确的反序列化触发点。我个人认为这个漏洞更多还是偏向于在已知存在反序列化之后要如何进行利用。这里就体现出和php中反序列化利用的不同之处,在php中找到反序列化的点以后必然是进行pop链的构造,而在java中存在反序列化触发点以后,可以进行pop链的构造,同时也可以结合框架特性进行组合攻击。这里漏洞思路我认为更偏向于利用框架特性进行攻击。
JNDI注入
漏洞入口点在于org/springframework/transaction/jta/JtaTransactionManager.java中的readObject方法。在java中使用readObject针对序列化数据进行反序列化时,会自动调用序列化数据存储对象的readObject方法。事实上我认为是调用了两次的readObject,第一次是针对输入的ObjectInputStream对象进行一个反序列化,这里调用的是ObjectInputStream类的readObject方法将序列化的字节码转化为对象A,这时再调用对象A中的readObject方法。我是将对象A的readObject方法理解为php中的魔术方法,当其被反序列化成功以后自动调用该方法。

跟入initUserTransactionAndTransactionManager方法:

虽然这个漏洞的利用方式并不是典型的pop链,但是对于反序列化漏洞的利用原则是一样的,就是说都是面对属性编程,所有属性都可控。在这里我们可控this.userTransactionName继续跟入lookupUserTransaction:

这里的getJndiTemplate方法用于获取this.jndiTemplate,而this.jndiTemplate在初始化时被赋值为JndiTemplate类的实例化对象。因此这里的lookup实际上调用的是JndiTemplate类中的方法。

这里的ctx是InitialContext类的实例化对象。该类的lookup方法实现资源引用,我的理解是用来注册服务提供者所提供的服务。如果把rmi理解为封装协议的话,这里的lookup方法我们就可以理解为针对rmi协议发送请求并解析。既然我们要解析rmi服务,那么必然存在一个rmi的服务提供者,为web应用提供服务。事实上这也是整个漏洞的重要危害点之一,这个服务提供者并不是一个可信的第三方,也不是本地,它可以是任意一台服务器。这里就引出一个问题,一旦InitialContext类的lookup方法参数可控,那么我们就可以在自己的服务器上构造一个rmi服务。这样一来当web应用就会访问我们注册的rmi服务。
RMI引入
仅仅只是请求rmi服务器没有任何意义,实际上我们可以通过请求rmi服务器来加载rmi服务器上的对象。而rmi服务器又可以通过绑定外部远程对象,因此我们可以将rmi服务器作为一个跳板,通过rmi服务器实现访问外部服务器上的字节码文件并加载。rmi服务器构造如下:
1 | public static void lanuchRMIregister() throws Exception { |
上面这段代码表明rmi服务器运行于1999端口。这里利用到的是rmi的动态加载类这一特性。所谓动态加载表示,当前jdk中没有这个类,那么就会通过指定的url去下载这个类的字节码文件。当客户端请求rmi服务器端尝试加载这些远程对象时,由于rmi服务器端本地并没有这一对象,因此rmi服务器端会将class字节码文件的url返回给客户端,由客户端进行一个字节码文件下载并加载操作。这一点很重要,在rmi中还有一种加载方式是远程对象调用。
rmi的远程对象调用流程如下图:

从逻辑上来说rmi实现的远程对象调用是在client上调用server对象中的方法。但是实际的数据流是从client到stub,再由skeleton流向server。这里的stub我们可以理解为远程对象的一个代理,真实的操作数据,比如具体执行方法和参数都是由stub通过socket发给skeleton,再由skeleton发送给server,在server中执行相应的方法,再把结果返回给client。
远程对象调用和动态加载最大的区别就是:远程对象调用是在远程服务器上执行代码后返回结果,而动态加载是将字节码文件下载到服务器本地再进行加载。对于漏洞利用而言,我们要的是将字节码文件下载到服务器本地进行加载,这样才能实现命令执行,而远程对象调用执行的是我们自己服务器上的命令,毫无意义。
在了解rmi服务器的功能之后,我们就可以尝试利用JNDI注入一个rmi远程对象,rmi服务自然是搭建在我们自己的服务器上。rmi远程对象的引用指向我们可控的evil.class,比如以下这种:
1 | public class ExportObject { |
将其编译成class字节码文件,放在外网可访问的服务器上,通过rmi远程加载这个对象即可。
到这里整个攻击流程算是分析结束。回头复盘整个流程我们会发现,实际上它和反序列化并没有很密切的关系,唯一相关的一个点也仅仅是因为实现JNDI的触发点在readObject方法中,在反序列化的时候会完成这一整套攻击流程。几乎可以说并不存在构造pop链的环节,在我看来反序列化之后的利用流程是:执行代码->利用JNDI特性->借助rmi服务器进行对象注入。也确实是实现对象注入,但是相对传统的pop链来说,还是有很大不同的。我个人更愿意将其理解为JNDI注入。
既然在整个漏洞中,JNDI才是最主要的一环,那么在除了反序列化点之外的地方是否还存在JNDI注入呢?
漏洞复现代码
1 | package com.jnditest; |
1 | package com.jnditest; |
1 | public class ExportObject { |
将ExportObject.java放置于公网服务器上,编译为class字节码文件。理论上来说SpringPOC可以放在其他服务器上,但是为了复现方便,我是将SpringPOC.java和ExploitableServer.java都放在本地。
最后弹一个计算器:

参考文章
https://www.freebuf.com/column/189835.html
https://blog.csdn.net/lmy86263/article/details/72594760