首先是一些JAVA审计中很重要的知识,其与JAVA的基础知识关系不大。
RMI的调用流程、RMI注册表以及动态加载类
RMI的调用流程,从我之前写过的 真实环境下的FastJson1.2.47反序列化 参考,首先POST到服务器一串Payload
该Payload有我们的恶意class的服务器地址和恶意路径。
受害者服务器访问该链接去调取恶意class。并执行。
其中Exploit.class由Exploit.java生成如图所示:
而这里的Exploit.class即为远程对象。
而远程方法调用则是技术,其中可选用rmi和ldap
1.2 远程对象
使用远程方法调用,必然会涉及参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化(实现java.io.Serializable 接口),并且客户端的serialVersionUID字段要与服务器端保持一致。
1 | serialVersionUID: |
对RMI而言:任何可以被远程调用方法的对象需要实现下列三种情形的一种
- 实现 java.rmi.Remote 接口
- 远程对象的实现类必须继承UnicastRemoteObject类。
- 手工初始化远程对象,并在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法。
如下是三种情形示范:
第一种实现 java.rmi.Remote 接口
1 | import java.rmi.Remote; |
第二种远程对象的实现类必须继承UnicastRemoteObject类
1 | public class Main extends UnicastRemoteObject { |
第三种手工初始化远程对象,并在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法
1 | public class HelloImpl implements IHello { |
在JVM之间通信时,RMI对远程对象和非远程对象的处理方式不一样, 它并没有直接把远程对象复制一份传递给客户端,而是传递了**一个远程对象的Stub **。
Stub:
Stub基本上相当于是远程对象的引用或者代理。Stub对开发者是透明的,客户端可以像调用本地方法一样直接通过它来调用远程方法。
Stub中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节
因此,Stub对于RMI通信至关重要,前面提到了其对开发者是透明的。如何获取Stub的方式值得考虑。
常见的方法是调用某个远程服务上的方法,向远程服务获取 。
JDK提供了一个RMI注册表(RMIRegistry), RMIRegistry也是一个远程对象,默认监听在传说中的1099端口上,可以使用代码启动RMIRegistry,也可以使用rmiregistry命令。
同时,在之前文章中遇到了rmi无法执行而ldap可以执行的情况。其原因应该就在于使用rmi的时候使用的远程对象(恶意服务器获取恶意服务器上的HTTP远程对象(也就是那个恶意的class))是没有实现三种rmi远程对象的要求的任意一种的:
动态类加载
- RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class。
- JVM中没有某个类的定义解释:启动时将相关类的class加载入JVM中,位于JAVA文件的第三个生命周期。
- 动态加载的对象class文件可以使用Web服务的方式进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。
- 对于客户端而言,服务端返回值也可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,则同样需要有运行时动态加载额外类的能力。客户端使用了与RMI注册表相同的机制。RMI服务端将URL传递给客户端,客户端通过HTTP请求下载这些类。
- JNDI注入的利用方法中也借助了动态加载类的思路
第一步:RMIServer注册1099的Registry服务
第二步:RMIClient想要与RMIServer通信
第三部:RMIClient通过注册表获取与Server通信的Stub连接并要求获取某个Object
第四步:RMIServer通过HTTP协议拿到WebServer的RMIClient想要的Object对象并在第五步通过Stub返回给RMIClient
JNDI注入
JNDI底层支持RMI远程对象,RMI注册的服务可以通过JNDI接口来访问和调用。
JNDI接口在初始化时,可以将RMI URL作为参数传入
在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还 **可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。 **
因此在原本的rmiServer.java中多了一步
整个利用流程如下:
- 目标代码中调用了InitialContext.lookup(URI),且URI为用户可控(也就是RCE的唯一条件,后面都是攻击者设计的路);
- 攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server//name;
- 攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;
- 目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.**getObjectInstance()**获取外部远程对象实例;
- 攻击者可以在Factory类文件的**构造方法、静态代码块、getObjectInstance()**方法等处写入恶意代码,达到RCE的效果;
同时上文提供了三种可执行代码的区块分别是: 构造方法、静态代码块、getObjectInstance()
Fastjson对于JDNI的调用链:
- -> RegistryContext.decodeObject()
- -> NamingManager.getObjectInstance()
- -> factory.getObjectInstance()
- -> NamingManager.getObjectInstance()
Spring框架的spring-tx.jar调用链
- JtaTransactionManager.readObject()
- InitialContext.lookup(URL)
com.sun.rowset.JdbcRowSetImpl类的execute() 调用链
- -> JdbcRowSetImpl.execute()
- -> JdbcRowSetImpl.prepare()
- -> JdbcRowSetImpl.connect()
- -> InitialContext.lookup(dataSource)
- -> JdbcRowSetImpl.connect()
- -> JdbcRowSetImpl.prepare()
有 jdk 版本要求
需要 target 主机上面的 jdk 版本有严格的要求:
具体是在新版本的JDK中trustURLCodebase值为false
导致Java\jdk8u202\jre\lib\rt.jar!\com\sun\jndi\rmi\registry\RegistryContext.class#354行抛出了异常:
该异常调用栈为
因此也造成了JDK自带的修复。而LDAP修复更晚,也就是说,尽量使用ldap来使用此攻击链(能用RMI必然能用LDAP)
Java反序列化:基于CommonsCollections4的Gadget分析
在反序列化漏洞的利用过程中,攻击者会构造一系列的调用链以完成其攻击行为。如何高效的生成符合条件且可以稳定利用的攻击Payload成为了攻击链条中的重要一环,当前已经有很多现成的工具帮助我们完成Payload的生成工作如 ysoserial
基于PriorityQueue类的序列化对象的构造,另一方面是PriorityQueue对象在反序列化过程中恶意代码的触发原理