通过动态调试了解Java RMI底层通信

0 前言       

       为了明白ysoserial中提供的exploit中的JRMPClient与JRMPListener攻击原理,于是去动态调试了Java RMI来了解其底层通信原理,在此做个记录:

1 RMI服务端

首先先调试RMI服务端的代码,如下是我编写的简易RMIServer代码:

public class RMIServer {
    public static void main(String[] args) {
        try {
            HelloServiceImpl obj = new HelloServiceImpl();
            //HelloServiceImpl没有继承UnicastRemoteObject,需使用exportObject来处理
            IHelloService helloService = (IHelloService) UnicastRemoteObject.exportObject(obj, 0);
            //创建Registry,监听于9999端口
            Registry reg = LocateRegistry.createRegistry(9999);
            //将HelloServiceImpl绑定到Registry
            reg.bind("HelloService", helloService);
            System.out.println("HelloServiceImpl已绑定到Registry ......");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1、在如下位置设置断点:

2、这时会进入UnicastRemoteObject的exportObject(Remote obj, int port)如下:

在该方法中,会调用方法exportObject(Remote obj, UnicastServerRef sref),其中会new一个UnicastServerRef对象,进入UnicastServerRef类,会发现其父类中包含一个LiveRef类型的属性:

查看LiveRef源码,看到其中会有一个Endpoint类型的属性,并在下面的构造方法中给其赋值了一个TCPEndpoint类型的对象值:

 TCPEndpoint类具有如下属性,主要包含ip地址与port等信息,该信息应该就是导出的远程对象对应的信息:

3、接着进入了UnicastRemoteObject的exportObject(Remote obj, UnicastServerRef sref)方法,这时会判断obj是否为UnicastRemoteObject类型,由于obj为自定义的HelloServiceImpl,未继承UnicastRemoteObject,因此if条件不成立,直接去调用sref的exportObject方法

4、这时进入UnicastServerRef类的exportObject(Remote var1, Ojbect var2, boolean var3),可以看到会先创建一个代理对象,继续查看源码,可以知道该代理对象类型为HelloServiceImpl,handler为RemoteObjectInvocationHandler,其中handler会包括上面创建的LiveRef对象(前面也知道了该对象中包含Endpoint等通信所需信息),因此可以判断在远程调用该对象时,客户端获取到的其实是该代理对象,再往下看,会生成一个Target对象,可以看到该Target对象包含了许多数据(导出的原始对象,创建的代理对象等)。然后该Target对象又被this.ref的exportObject方法导出,于是后面跟入该方法。

5、于是上面创建的LiveRef类的exportObject(Target var1)方法:

6、继续跟入,进入了TCPEndpoint类的export(Target var1)方法,上面已经知道TCPEndpoint中包含着ip和port等通讯所需信息:

7、继续跟进,发现进入了TCPEndpoint类中的属性TCPTransport类的export(Target var1)方法,其中会执行listen()方法,该方法就是为导出的对象去开启一个socket通信端口:

跟入listen方法如下,首先获取TCPEndpoint对象,然后使用该对象去创建一个socket服务,下图可以看到socket端口为0,肯定是无效的,那就继续往下走。

 执行完上面一步代码后,看下图,发现socket端口变成了62111,经过多次测试,发现该值是随机的,每次运行都不一样,再下一步代码就是开启一个单独的线程,来进行socket通信:

所以到了这里,我们就可以知道,每一个导出的对象都会单独开启一个socket,并且用一个单独的线程来处理socket通信,因此知道服务端不只是有一个Registry监听端口,而是所有导出对象都会有一个监听端口,且该端口值是随机生成的。 

8、继续运行断点到如下位置,:

9、进入LocateRegistry类的createRegistry(int port)方法,继续跟进:

 10、进入RegistryImpl类的构造方法RegistryImpl(int var1),继续跟进。

11、进入 RegistryImpl类的setup(UnicastServerRef var1)方法:

12、 这时又进入UnicastServerRef类的exportObject(Remote var1, Object var2, boolean var3)方法,后面的流程就跟上面第4步后面的一样了。可以看到这里导出的对象是RegistryImpl,并且这时会进入如下红框部分,执行setSkeleton方法,该方法中会生成一个RegistryImpl_Skel对象,猜测该对象就是其他文章中经常提到的服务端的skeleton对象。

13、然后运行代码到如下位置:

 14、这里会从bindings(就是一个Hashtable)中获取key=HelloService的value,如果获取到会抛出异常,说明已经注册了该命名的对象,如果没获取到,则将该键值对put进Hashtable中。

15、到此,整个RMIServer端的代码已经执行完了,总结一下:

(1)每一个导出的对象都有一个单独的socket,socket端口值是随机生成的,并且用一个单独的线程来处理socket通信。

(2)服务端传递给客户端的远程对象其实是一个代理对象,且该代理对象的handler为RemoteObjectInvocationHandler,该handler包含远程通信所需的所有信息。(这里也解释了我前一篇文章JEP 290之后攻击Java RMI服务中为什么要在RemoteObjectInvocationHandler类中下断点来修改调用远程方法时的参数值)

(3)通过Registry.bind方法绑定的远程对象,会存到Hashtable中。

2 RMI客户端

如下我写的简易RMI客户端代码:

public class RMIClient {
    public static void main(String[] args) throws Exception {
        //根据ip和端口获取Registry
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
        //使用Registry获取远程对象的引用
        IHelloService services = (IHelloService) registry.lookup("HelloService");
        // 使用远程对象的引用调用对应的方法
        String res = services.sayHello("我是RMIClient ...... ");
        System.out.println(res);
    }
}

1、在如下地方设置断点:

2、进入LocateRegistry类的getRegistry(String host, int port)方法,继续跟进:

3、进入LocateRegistry类的getRegistry(String host, int port, RMIClientSocketFactory csf),可以看到返回的是一个代理对象:

4、运行到如下位置,可以看到上步返回的代理对象为RegistryImpl_Stub类型,但是该类无法无法调试,猜测应该是运行时的class文件与jdk源码的class文件不一致,导致无法调试。

5、所以直接看RegistryImpl_Stub的源码,找到lookup方法,可以看到有如下关键调用代码:

6、首先进入UnicastRef类的newCall方法,该类是可以进行调试的,于是在该方法中下断点:

 首先是获取了一个TCP连接,可以看到是使用LiveRef去创建的连接,在调试RMIServer时,我们已经知道LiveRef中包含TCPEndpoint属性,其中包含ip与端口等通信信息:

 再往下走,看到new了一个StreamRemoteCall对象,进入StreamRemoteCall的构造方法,其做了如下操作,往服务端发送了一些数据:

7、然后再回到第5步,继续往下走,执行了ObjectOutput.writeObject,这里是将lookup方法中传递的远程服务的名称,即字符串“HelloService”进行了序列化并发往了服务端,然后又执行了super.ref.invoke方法,进入该方法如下,然后继续往下走,

8、进入StreamRemoteCall类的executeCall方法,可以猜到该方法就是处理第7步往服务端发送数据后的服务端响应的数据,看到从响应数据中先读取了一个字节,值为81,然后又继续读取一个字节赋值给var1,

 下面是判断var1的值,为1直接return,说明没问题,如果为2的话,会先对对象进行反序列化操作,然后判断是否为Exception类型(网上有关于带回显的攻击RMI服务的exp,它就是将执行完命令后的结果写到异常信息里,然后抛出该异常,这样在客户端就可以看到命令执行的结果了,这时得到的var1的值就是2)

9、当上一步var1值为1时,说明没问题,再回到第5步的图,会执行ObjectInput.readObject方法将服务端返回的数据反序列化,然后将该对象返回(前面我们也知道了,这里获取到的其实是一个代理对象)。至此,客户端整个请求的过程也梳理完了,总结一下:

(1)客户端通过LocateRegistry类的getRegistry方法获取的是RegistryImpl_Stub类型的对象

(2)在UnicastRef类的newCall方法中与服务端建立Socket连接,并发送一些约定的数据

(3)通过ref.invoke方法处理服务端响应回来的序列化数据。

3 服务端响应数据给客户端

1、由于客户端通过RegistryImpl_Stub.lookup(String var1)方法调用时最终调用的是服务端的RegistryImpl.lookup(String var1),因此在服务端的RegistryImpl.lookup(String var1)方法中下断点启动服务端,当运行客户端代码时,就会停在该方法,如下:

这是查看调用栈即可看到整个调用过程:

2、进入TCPTransport类的handleMessages(Connection var1, boolean var2)方法中,可以看到熟悉的身影,下面从客户端发送的数据中读取了一个数据,值为80,就是在上面第2章第6点中的StreamRemoteCall类的构造方法中向服务端发送的80。

3、继续跟入Transport类的serviceCall(final RemoteCall var1)方法中,发现先是读取ObjID,然后又根据ObjectEndpoint获取了Targer对象,然后调用了该Target对象中的Dispatcher对象的dispatch(Remote var1, RemoteCall var2)方法。继续跟进

4、最终调用到了UnicastServerRef类的dispatch(Remote var1, RemoteCall var2)方法,如下,可以看到var40.readInt()方法读取的是上面第2章第6点中的StreamRemoteCall类的构造方法中向服务端发送的int数据2(由于调试时显示的数据有错误,具体原因应该是运行时的class和源码中的class不一致导致的,但可以看到var4的值为2,其实应该是var3的值,这个问题经常出现,也没找到解决办法。)

5、继续跟进到oldDispatch方法中,可以看到获取的long型数据即为上面第2章第6点中的StreamRemoteCall类的构造方法中向服务端发送的long型数据4905912898345647071

6、继续跟入RegistryImpl_Skel类中的dispatch(Remote var1, RemoteCall var2, int var3, long var4)方法中,可以看到将上面获取到的2和4905912898345647071均传递给了该方法,可以看到首先会校验long型的数据,然后会进入switch中判断int的值

switch中会进入case2分支,这里会先将上面第2章第7步中序列化后的字符串“HelloService”进行反序列化,然后再调用RegistryImpl类的lookup(String var1)方法去查询相应的对象,最后将查询得到的对象再进行序列化后响应给客户端。

7、上面已经完成了整个服务端响应给客户端数据的整个通信流程,然后我们再看一下RegistryImpl类的lookup(String var1)方法的实现如下,很简单,就是从bindings中获取key=HelloService的value值。

总结一下,也写啥可总结的,就是在交换一些数据,在第1步的调用栈中也把交换数据的流程写明了。 

4 总结

终于理清了整个RMI的客户端与服务端进行通信的一些细节。

最后说下,看源码一定要结合调试来看,然后就是坚持就是胜利!!!

 

参考文章:

https://blog.csdn.net/sinat_34596644/article/details/52599688

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值