1、RMI是什么

Java RMI用于不同虚拟机之间的通信,这些虚拟机可以在不同的主机上、也可以在同一个主机上;一个虚拟机中的对象调用另一个虚拟上中的对象的方法,只不过是允许被远程调用的对象要通过一些标志加以标识。这样做的特点如下:

优点:避免重复造轮子;

缺点:调用过程很慢,而且该过程是不可靠的,容易发生不可预料的错误,比如网络错误等;

在RMI中的核心是远程对象(remote object),除了对象本身所在的虚拟机,其他虚拟机也可以调用此对象的方法,而且这些虚拟机可以不在同一个主机上。每个远程对象都要实现一个或者多个远程接口来标识自己,声明了可以被外部系统或者应用调用的方法(当然也有一些方法是不想让人访问的)。

2、RMI 基本实现

客户端

服务端

RMI 客户端通过远程调用服务端的 sayHello 方法,成功在服务端输出了大写的“HELLO”。

3、流程原理

3.1 服务端-创建远程服务

调试分析远程对象的创建过程:

进入其构造函数,

**UnicastRemoteObject **中 ,port 被默认赋值为 0

步入 UnicastRemoteObject 类中的 exportObject (姑且称为导出对象函数)

  • public static:不需要创建 UnicastRemoteObject 实例也可以调用
  • Remote obj:实现了 java.rmi.Remote 接口的对象,
  • int port: 接收客户端的请求

接下进入 return exportObject(obj, new UnicastServerRef(port)); 中的 new UnicastServerRef(port)),UnicastServerRef(称之为服务端引用)

var 1 这里是端口号 port ,传入 port 后,调用 父类构造函数 LiveRef,

来到 LiveRef 的构造函数

这里可以看一下 getLocalEndpoint

这里的 TCPEndpoint 用于远程通信,其构造函数中的变量 String var1 为 host (ip) 、int var2 为 port (端口)

继续跟入 this(var1, TCPEndpoint.getLocalEndpoint(var2), true); 中的 this,在此处 ep 中有了IP和端口信息

  • Endpoint ep 端点信息(IP、端口、socket)
  • Channel ch 通信通道(缓存的连接)
  • isLoacl 是否为本地对象

然后这些信息都被存放在 LiveRef 中。

接下来又调用了父类方法并将 ref 赋值为 var1(var1:LiveRef@626)

接下来回到 return exportObject(obj, new UnicastServerRef(port)); 中的 exportObject 方法

首先判断了 obj 是否继承了 UnicastRemoteObject ,如果没有继承那么就把 ref 设置为 sref。

之后都使用 sref.exportObject 进行工作完成导出远程对象的操作

而 sref 中封装着 LiveRef@626

**sref.exportObject ** 的执行过程:

stub = Util.createProxy(implClass, getClientRef(), forceStubUse);这一步是创建客户端代理的过程。

(为什么要在服务端创建客户端代理呢?)

这里通过流程图解释:服务端首先创建好 Remote Stub 放到注册中心,客户端通过注册中心拿到 Remote Stub,客户端通过 Remote Stub 调用另一个代理 Remote Skeleton ,之后 Remote Skeleton 调用服务端。

步入 createProxy

clientRef 中存放的就是那个核心 LiveRef@783 (上文是 LiveRef@626,核心都是相同的)

此处进行了判断:

  • forceStubUse:如果为 true,就强制使用旧的 静态 Stub 类(由 rmic 工具生成的 _Stub.class)这是为了兼容 JDK 1.1 时代的 RMI,这种情况直接调用 createStub(...) 去加载和实例化 Stub
  • ignoreStubClasses:如果为 true,表示忽略静态 Stub 类。这时会尝试用 动态代理(java.lang.reflect.Proxy)来生成 Stub
  • stubClassExists:

如果要进入 createStub ,强制使用 StubforceStubUse == true,没有忽略 Stub 并且存在 Stub 类ignoreStubClasses == false``stubClassExists(remoteClass) == true

下一步创建动态代理

其中的 handler 存放 LiveRef@783

这样就创建好了动态代理 stub

进入 Target (实际上是将目前创建的东西都封装到一起)

客户端引用(stub)和服务端引用(disp)的 LiveRef 是一样的,

接下来,将封装好的 target 发布出去

跟到了 TCPTransport 类中的 exportObject

listen() 函数先获取 TCPEndpoint@823

准备创建 ServerSocket

判断 port 是否为 0,如果为 0 ,进入 setDefaultPort,设置端口号

进入 server.getLocalPort

在这个函数中端口被赋值,此端口随机,

这个时候的端口被赋值为 55685

  • doPrivileged: 表示即使调用栈上其他代码没有权限,也允许这里的操作按本方法的权限执行。
  • NewThreadAction: 创建一个新的线程

这个线程开启之后等待客户端的连接

做完这些之后,用 MAP 表储存 target,target 中包括了 IP、端口、服务端代理、客户端代理等信息

最后返回一些值,完成了服务端的发布过程。

到这里在返回看 exportObject 函数的作用:

  • 静态方法的 exportObject 传入俩个参数,创建一个 UnicastServerRef 对象 sref ,sref 中封装了IP、端口等信息,核心是 LiveRef@xxx,然后调用 exportObject(obj, sref)
  • 重载方法的 exportObject 返回代理对象 stub
  • 这样就做到了把一个本地的远程对象实例导出成一个可以远程访问的对象

3.2 注册中心-创建注册中心+绑定

调用 createRegistry 创建注册中心,默认端口号为1099

到了这一步点“恢复程序”,如果继续跟进去会发现一堆奇怪的东西

来到这一步:

创建了一个 LiveRef,一个 lref,(这里的作用和服务端的实际上是一样的)

3.3 客户端-请求注册中心

首先连接注册中心,接收传入的IP和端口

使用LIveRef ,将传入的 host,port,等封装

再次调用 createProxy 方法,同样的创建了 stub ,(这里的创建是通过注册中心传入参数(host,port..),客户端自行创建 stub)

来到下一步:

问题:在动态调试时遇到报错,可能是服务端没有开启!

下一步调用会报以下错误,因为服务端没有开启监听

将 RMIServer 运行起来就可以了

这里看到传进来的 var1 实际上就是IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");中的 remoteObj

跟进 invoke

super.ref.invoke(var2);

StreamRemoteCall 类中的 executeCall 是真正处理网络请求的方法

executeCall 中有一步通过反序列化处理异常,如果注册中心有恶意对象,客户端在此处反序列化时被攻击。

由于 executeCall 是客户端网络请求的必经之路,所以这个反序列化几乎不可避免

这里的 lookup 方法的作用就是去注册中心查找远程对象

3.4 客户端-请求服务端

同样注意开启服务端

来到了 invoke

这里又跟到了 StreamRemoteCall ,这样的请求同样可以被攻击

结束 call.executeCall(); 后会出现另一个反序列化点

进入 unmarshalValue

unmarshalValue 函数的作用就是根据目标类型,选择正确的方式从输入流中读取数据。

  • 对基本类型,用专门的 readxxx() 方法读取,再返回
  • 对对象类型,直接调用 readObject() 反序列化

returnValue 返回了 “HELLO”

总结-客户端产生的反序列化漏洞点

  1. StreamRemoteCall.executeCall

StreamRemoteCall 类中的 executeCall 是真正处理网络请求的方法

executeCall 中有一步通过反序列化处理异常,如果注册中心有恶意对象,客户端在此处反序列化时被攻击。

由于 executeCall 是客户端网络请求的必经之路,所以这个反序列化几乎不可避免


executeCall 处理的协议就是JRMP 协议

  1. unmarshalValue

unmarshalValue 函数向服务端请求获取返回值时,返回值是通过反序列化产生的。

3.5 注册中心-客户端请求时

前面说过,服务端创建了 stub 并将信息封装到 target,随后的 NewThreadAction 创建了一个新的线程,等待客户端的响应

这个线程开启之后等待客户端的连接

现在进入创建线程的流程:

进入 run() 后,只能调用 executeAcceptLoop,executeAcceptLoop 又创建了一个新的线程

run 调用了 run0

run0 前面都是在解析一些协议,重点是 handleMessages

其中的 默认情况是调用了 serviceCall

serviceCall 会从表中获取 target

此处断点调试:

可以看到断点处的 stub 就是服务端创建好的东西

之后会调用 disp.dispatch,disp 是一个分发器,用于将远程请求分发到服务端执行

这里 skel 不为空就会调用 oldDispatch

skel.dispatch

而 skel.dispatch 是属于 RegistryImpl_Skel 类中的 dispatch

接下来就到了重点:

RegistryImpl_Skel.class

总结-注册中心产生的漏洞点

  1. 客户端在请求时 (IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");),lookup的对象是序列化后传到注册中心的,而注册中心的 **RegistryImpl_Skel **中,大部分 case 的函数都有反序列化。如果是一个恶意序列化对象,在注册中心运行到 **RegistryImpl_Skel **时,会产生反序列化漏洞 var98 = (String)var104.readObject();

3.6 服务端-客户端请求时

过程和注册中心被客户端请求时一样,但要注意,调试的时候需要代理是动态代理,按 F9 让程序往下运行直到得到动态代理。

图中的 RegistryImpl_Stub DGCImpl_Stub 都不是想要的

最终拿到的 $Proxy0 是我们需要的动态代理

同样来到 disp.dispatch

不一样的是在 skel != null判断时,此时的 skel 为空,不进入 oldDispatch

继续走,获取远程方法 sayHello

之后会进入 unmarshalValue,在 客户端产生的反序列化漏洞点 中也有 unmarshalValue

最终在 result = method.invoke(obj, params); 这一步完成远程调用

3.7 DGC-客户端请求服务端

分布式垃圾回收,又称DGC,RMI使用DGC来做垃圾回收,因为跨虚拟机的情况下要做垃圾回收没办法使用原有的机制。我们使用的远程对象只有在客户端和服务端都不受引用时才会结束生命周期。

而既然RMI依赖于DGC做垃圾回收,那么在RMI服务中必然会有DGC层,在yso中攻击DGC层对应的是JRMPClient,在攻击RMI Registry小节中提到了skel和stub对应的Registry的服务端和客户端,同样的,DGC层中也会有skel和stub对应的代码,也就是DGCImpl_Skel和DGCImpl_Stub,我们可以直接从此处分析,避免冗长的debug。

而客户端一方在使用服务端的远程引用时需要调用dirty来注册,在用完时需要调用clean进行清除。

DGCImpl_Stub

clean 就是”强”清除内存,dirty 就是”弱”清除内存

这里调用了 readObject() 方法,存在反序列化的入口类。

DGCImpl_Skel

也存在漏洞点

4、RMI的几种攻击方式

https://drun1baby.top/2022/07/23/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9802-RMI%E7%9A%84%E5%87%A0%E7%A7%8D%E6%94%BB%E5%87%BB%E6%96%B9%E5%BC%8F/

4.1 攻击注册中心

攻击点还是在 RegistryImpl_Skel 代码中的反序列化,

case 的对应关系如下:

  • case0 – bind
  • case1 – list
    • list 这里没有 readObject ,无法攻击
  • case2 – lookup
  • case3 – rebind
  • case4 – unbind

4.1.1 bind&rebind

调用 bind 时,会反序列化参数名和远程对象,如果服务端存在 cc 链

EXP:

首先导入 commons-collections 3.2.1

需要注意,服务端和攻击端的项目应一致——都是 maven ,否则攻击无效;如果不在一个项目,都要添加 commons-collections 依赖。

rebind 和 bind 一样,不再赘述。

4.1.2 lookup&unbind

lookup 只能传入一个 String 类型

分析 lookup 的功能,然后伪造 lookup 代码,达到目的:

调用 invoke 后就到了客户端请求注册中心的流程:

EXP:

在 bind 的基础上修改:

4.2 攻击客户端

4.2.1 注册中心攻击客户端

除了 unbind 和 rebind 都会返回数据给客户端,返回的数据是序列化形式,那么到了客户端就会进行反序列化,如果我们能控制注册中心的返回数据,那么就能实现对客户端的攻击,这里使用ysoserial 的 JRMPListener,因为 EXP 实在太长了。

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'calc'

客户端去访问:

但是运行完什么都没有发生。。

后续发现俩个问题,
一是 Java 版本

如果是用CC1链,Java版本低于 1.8.0_65

当然,高版本可以用 CC6 链绕过也是可以的

二是运行计算器时因为加了 单引号 ’’ 而找不到程序

运行 calc "calc" 都可以正常弹出

根据 AI 的解释:在Windows系统中,cmd 命令行参数的单引号不会被自动去除,而是作为参数的一部分传递。当ysoserial尝试执行命令时,它接收到的是带单引号的 'calc',而不是单纯的 calc。当我们换成 power shell 时,是可以正确执行的。

img

4.2.2 服务端攻击客户端

服务端攻击客户端,可分俩种情形:

  1. 服务端返回Object对象
  2. 远程加载对象
服务端返回Object对象

RMI 远程方法调用返回的不一定是一个基础数据类型(比如String int),也会返回一个对象。服务端返回给客户端一个对象,客户端要对这个对象反序列化。所以我们伪造一个服务端,当客户端调用某个方法时,返回的就是恶意对象,就可以攻击客户端。

User 接口,返回 Object 对象

服务端实现 User 接口,返回 CC1 恶意 Object 对象

恶意服务端将恶意对象注册到注册中心

客户端获取恶意对象,调用 getUser() 方法,反序列化恶意远程对象

远程加载对象

《Java 安全漫谈》

codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的

CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。

如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则

Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为

Example类的字节码。

RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻

找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在

本地没有找到这个类,就会去远程加载codebase中的类。

这个时候问题就来了,如果codebase被控制,我们不就可以加载恶意类了吗?

对,在RMI中,我们是可以将codebase随着序列化数据一起传输的,服务器在接收到这个数据后就会去

CLASSPATH和指定的codebase寻找类,由于codebase被控制导致任意命令执行漏洞。

不过显然官方也注意到了这一个安全隐患,所以只有满足如下条件的RMI服务器才能被攻击:

  • 安装并配置了SecurityManager
  • Java版本低于7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false

其中 java.rmi.server.useCodebaseOnly 是在Java 7u21、6u45的时候修改的一个默认设置:

https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html

https://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html

官方将 java.rmi.server.useCodebaseOnly 的默认值由 false 改为了 true 。在 java.rmi.server.useCodebaseOnly 配置为 true 的情况下,Java虚拟机将只信任预先配置好的 codebase ,不再支持从RMI请求中获取。

4.3 攻击服务端

这一部分同样是熟悉的 unmarshalValue ,

服务端调用方法时,存在非基础类型的参数时,就会被恶意 Client 端传入恶意数据触发反序列化

参考文章:

官方文档

https://docs.oracle.com/javase/tutorial/rmi/overview.html

从懵逼到恍然大悟之Java中RMI的使用

https://blog.csdn.net/lmy86263/article/details/72594760

JAVA安全基础(四)– RMI机制

https://xz.aliyun.com/news/8760

一文回顾攻击Java RMI方式

https://www.anquanke.com/post/id/263726#h2-5

Java RMI 攻击由浅入深

https://su18.org/post/rmi-attack/

https://drun1baby.top/2022/07/23/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9802-RMI%E7%9A%84%E5%87%A0%E7%A7%8D%E6%94%BB%E5%87%BB%E6%96%B9%E5%BC%8F/

https://www.cnblogs.com/pihaochen/p/11020596.html

https://xz.aliyun.com/t/9053

https://xz.aliyun.com/t/7930

https://xz.aliyun.com/t/6660

https://xz.aliyun.com/t/7079