RMI先容
RMI全程Remote Method Invocation (长途方法引用),RMI有客户端和服务端,尚有一个注册中央,在java中客户端可以通过RMI调用服务端的方法,流程图如下:
服务端创建RMI后会在RMI Registry(注册中央)注册,之后客户端都是从注册中央调用方法,RMI分为三个主体部分:
- Client-客户端:客户端调用服务端的方法
- Server-服务端:长途调用方法对象的提供者,也是代码真正实行的地方,实行结束会返回给客户端一个方法实行的效果
- Registry-注册中央:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用,在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功
RMI的简单使用
服务端
准备三个文件,接口,实现接口的类,服务端- package org.example.server;
- import java.rmi.Remote;
- import java.rmi.RemoteException;
- public interface RMIMethodServer extends Remote {
- public String sayHello(String key) throws RemoteException;
- }
复制代码- package org.example.server.impl;
- import org.example.server.RMIMethodServer;
- import java.rmi.RemoteException;
- import java.rmi.server.UnicastRemoteObject;
- public class RMIMethodImpl implements RMIMethodServer {
- public RMIMethodImpl() throws RemoteException {
- UnicastRemoteObject.exportObject(this, 0);
- }
- @Override
- public String sayHello(String key){
- System.out.println(key.toUpperCase());
- return key.toUpperCase();
- }
- }
复制代码- package org.example;
- import org.example.server.RMIMethodServer;
- import org.example.server.impl.RMIMethodImpl;
- import java.rmi.AlreadyBoundException;
- import java.rmi.RemoteException;
- import java.rmi.registry.LocateRegistry;
- import java.rmi.registry.Registry;
- public class RMIServer{
- public static void main(String[] args) throws RemoteException, AlreadyBoundException {
- RMIMethodServer remoteServer = new RMIMethodImpl();
- // 注册中心
- Registry r = LocateRegistry.createRegistry(7788);
- // 绑定对象到注册中心
- r.bind("remoteServer", remoteServer);
- }
- }
复制代码 客户端
- package org.example;
- import org.example.server.RMIMethodServer;
- import java.rmi.NotBoundException;
- import java.rmi.RemoteException;
- import java.rmi.registry.LocateRegistry;
- import java.rmi.registry.Registry;
- public class RMIClient {
- public static void main(String[] args) throws RemoteException, NotBoundException {
- Registry r = LocateRegistry.getRegistry("127.0.0.1", 7788);
- RMIMethodServer remoteServer = (RMIMethodServer) r.lookup("remoteServer");
- remoteServer.sayHello("I am handsome");
- }
- }
复制代码 先运行服务端,在运行客户端,可以看到服务端有输出
RMI创建流程分析
创建长途对象
第一步创建实现类对象
我们的实现类继承了UnicastRemoteObject
下一步进入UnicastRemoteObject的构造方法
导出我们的长途对象
继续跟进,又调用了另一个同名方法,UnicastServerRef是封装RMI的入口
跟进UnicastServerRef,构造方法调用了父类的构造方法,记住LiveRef这个对象,这是核心类,背面所用到的别的大部分类都是对这个类的封装
继续跟进LiveRef,又调用了另一个类的构造方法
这个ObjID没啥用,就是一个标识,继续跟进,到这才是核心,TCPEndpoint.getLocalEndpoint(port)这里才真正调用了长途服务对象的信息
继续跟进,到这,能看出一些关于网络哀求的信息
继续跟进,回到LiveRef,这里有一些赋值
实行完后就回到了UnicastServerRef,下一步对ref举行了赋值,这里是第一次封装
末了回到了UnicastRemoteObject,可以看到sref就是LiveRef的封装
继续跟进,进入Util.createProxy这个方法
getClientRef获取LiveRef的封装
进入createProxy,加载长途类,判断这个类是否有_Stub后缀,这里显然没有
下面是一个创建动态署理的过程
创建完后回到stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
然后创建一个Target类,这里其实就是把这一大堆东西又举行一次封装
接下来是exportobject的套娃,LiveRef -> TCPEndpoint -> TCPTransport,进入listen方法
是一个建立socket的过程,内里对port举行了一个初始化,随机指定了一个端口
listen过后又来到父类Transport的exportobject
跟进putTarget方法,将我们层层封装的Target放入一个Map(objtable)中结束
创建注册中央和绑定
进入createRegistry
进入RegistryImpl,我们用的不是默认端口1099,所以直接看else
new一个LiveRef,然后用UnicastServerRef封装,进入setup方法,调用exportobject,到这是不是很眼熟,我们创建长途对象的时候走过
进入createProxy方法,这次就跟上次不一样了,由于RegisryImpl_stub这个类存在,所以进入if
进入createStub方法,这里有个反射实例化的过程,实例化了这个RegisryImpl_stub类
实行完后跳出去,这里设置了Skeleton(骨架)
从一开始的流程图我们知道,RMI是一个对称分布的结构,客户端有stub,服务端就对应有Skeleton,客户端通过stub哀求长途方法,服务端就通过Skeleton去调用方法,然后通过Skeleton获取效果,末了传给客户端Stub,客户端就从Stub获取效果,因此继续跟进setSkeleton方法:
进入createSkeleton方法,反射获取Skeleton
之后又进入Target的构造方法,跟之前不同的地方是disp中的skel有值了
接着又是exportobject的套娃,来到TCPTransport进入listen方法,创建socket,之后一系列的利用跟第一步相同,返回一个stub
客户端哀求注册中央(客户端被攻击)
这里重点在注册后使用了lookup方法,我们调试进去,调试个鸡毛,这是个class文件
直接硬看
漏洞点一
先获取了一个输出流,获得服务端序列化效果,之后在背面调用readObject反序列化,这里一看就有漏洞,如果服务端的返回是恶意的,直接就被rce了
漏洞点二
super.ref.invoke这东西看着就像漏洞,这里的ref是UnicastRef,我们跟进去
call.executeCall是重点,这里call其实StreamRemoteCall,进入StreamRemoteCall查看executeCall方法,在末尾的异常处理调用了in.readObject,这里也能被攻击
漏洞点三
我们调用的是lookup方法,那别的方法是不是也能使用,一看,全都调用了invoke方法,攻击点都是漏洞点二
客户端哀求服务端(客户端被攻击)
漏洞点分析
由于获取的remoteServer是动态署理类,所以会调用invoke方法
跟进调试,RemoteObjectInvocationHandler
其中的invokeRemoteMethod,看名字知道是方法调用,跟进一看,有ref.invoke
跟进invoke,有个marshalValue
继续跟进,这是判断参数类型的,末了序列化,这里没判断String类型,所以用String类型可以到
这
出来了,又调用了call.executeCall(),上面的漏洞点二,攻击手法一样,然后会判断返回值是否为空
不为空调用unmarshalValue,进去一看,同样是判断类型,然后反序列化,这就能反序列化攻击了
客户端哀求服务端(注册中央被攻击)
创建注册中央的流程走一边走到listen里,看这个线程
跟进AcceptLoop,注意这个executeAcceptLoop
继续跟进,这里的execute又开了一个线程池
继续跟进,进入ConnectionHandler的run方法,也就是run0方法
继续跟进,StreamRemoteCall这可太熟了,但是它不是重点
继续跟进serviceCall
放弃。。。这里怎么断点断不住,直接手动进去查看
进入dispatch方法,这里是UnicastServerRef的方法
跟进oldDispatch,skel是Registry_Impl_Skel
跟进dispatch方法,case 2中反序列化
然后这里case对应环境如下
- bind : 0
- list : 1
- lookup : 2
- rebind : 3
- unbind : 4
如果客户端传入恶意对象,那注册中央就会被反序列化攻击
客户端哀求服务端(服务端被攻击)
回到上面的oldDispatch处,过了这个继续往下,调用unmarshalValue,很熟
判断是否为基本类型,传入Object,同样反序列化,寄
客户端哀求服务端(DGC)
DGC就是RMI里垃圾回收机制,具体先容如下:
分布式垃圾回收,又称 DGC,RMI 使用 DGC 来做垃圾回收,由于跨虚拟机的环境下要做垃圾回收没办法使用原有的机制。我们使用的长途对象只有在客户端和服务端都不受引用时才会结束生命周期。
而既然 RMI 依赖于 DGC 做垃圾回收,那么在 RMI 服务中必然会有 DGC 层,在 yso 中攻击 DGC 层对应的是 JRMPClient,在攻击 RMI Registry 小节中提到了 skel 和 stub 对应的 Registry 的服务端和客户端,同样的,DGC 层中也会有 skel 和 stub 对应的代码,也就是 DGCImpl_Skel 和 DGCImpl_Stub,我们可以直接今后处分析,制止冗长的 debug。
这个有点复杂,没太看懂,学会了再补充..............
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |