RMI介绍
RMI (Remote Method Invocation) 远程方法调用,顾名思义,它的功能就是就是实现调用远程方法
实现RMI的协议叫JRMP,RMI实现的过程中进行了java对象的传递,自然使用了序列化和反序列化,也自然产生了反序列化漏洞
这里介绍一下rmi的流程。
首先,rmi里有三个角色,分别是客户端、服务端以及注册中心
设计上客户端和服务端不直接通信,而是通过注册中心通信。简单理解三者的关系如下:服务端创建远程对象,并将远程对象在注册中心注册,客户端到注册中心端查找并获取对应的远程对象,最终在服务端调用其方法。
具体实现时,客户端没有直接调用服务器上的对象,也没有直接调用注册中心上的对象,而是操作一个进行网络通信的代理类叫Stub,服务端也一样有一个类似的代理类叫Skel,具体操作都是这两个代理类进行的。
RMI 引入了两个概念,分别是 Stubs(客户端存根) 以及 Skeletons(服务端骨架),当客户端(Client)试图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是 Skeleton,它从 Stub 中接收远程方法调用并传递给真实的目标类。Stubs 以及 Skeletons 的调用对于 RMI 服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过 Stub 和 Skeleton 来实现的。
整体调用时序图:
rmi demo
写个demo具体介绍一下rmi的利用
首先需要定义远程对象类。远程方法调用不是所有类都可以,想进行远程方法调用的类,需要实现一个继承Remote接口的接口,远程方法要抛RemoteException。先定义这个接口:
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteObj extends Remote {
// 远程方法要抛RemoteException
public String sayHello() throws RemoteException;
}
然后去定义实现类
这个实现类在后续需要被远程调用,服务端需要把这个远程对象发布出去,因此也有条件。
这个实现类必须调用UnicastRemoteObject.exportObject方法
这里有两个选择,一个是去继承UnicastRemoteObject,然后UnicastRemoteObject的构造方法就会自动去调用UnicastRemoteObject.exportObject。另一个是直接自己去调用UnicastRemoteObject.exportObject方法。
定义好的远程对象类
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class IRemoteObjImpl extends UnicastRemoteObject implements IRemoteObj{
protected IRemoteObjImpl() throws RemoteException {
}
@Override
public String sayHello() throws RemoteException {
String name = this.getClass().getName();
System.out.println(name);
return name;
}
}
远程对象类定义好后,也就是说我们现在有可以发布的东西了,接下来就可以去定义服务端和客户端了,服务端一般和注册中心写在一起,做如下几件事:
1、首先创建远程对象,创建时也会进行远程对象的发布。
2、创建注册中心
3、将远程对象绑定到注册中心
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
//创建远程对象
IRemoteObjImpl iRemoteObj = new IRemoteObjImpl();
//创建注册中心
Registry registry = LocateRegistry.createRegistry(1099);
//把远程对象发布到注册中心上,这里还提供了查询(lookup)、重新绑定(rebind)、接触绑定(unbind)、list(列表)的方式
registry.bind("iRemoteObj",iRemoteObj);
}
}
这里看到好像还可以用java.rmi.Naming类来实现
方式是Naming.bind("rmi://localhost:1099/iRemoteObj", iRemoteObj);
接下来是客户端,客户端也做三件事
1、获取“注册中心”对象
2、利用注册中心对象获取远程对象
3、调用远程对象上的方法
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 registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("iRemoteObj");
String s = remoteObj.sayHello();
System.out.println(s);
}
}
启动之后在服务端上可以看见输出的name,在客户端上也可以接收返回的name值并打印出来
源码分析
创建远程对象
IRemoteObjImpl iRemoteObj = new IRemoteObjImpl();
这行代码的作用就是创建远程对象
具体的执行过程是
首先调用IRemoteObjImpl的构造函数
然后就会进入UnicastRemoteObject的构造函数
再进入exportObject方法,会看到UnicastServerRef类,它代表远程对象的引用。这个类继承了Dispatcher接口,代表由它分发客户端的操作给远程对象。
根据注释能知道这里obj是要导出的远程对象,port是导出的端口,返回值是存根,也就是stub
再跟到UnicastServerRef里
这里出现了一个LiveRef类,接着跟进去
在LiveRef的构造方法里有一个TCPEndpoint类,显然和通信有关,再继续跟
public static TCPEndpoint getLocalEndpoint(int port) {
return getLocalEndpoint(port, null, null);
}
public static TCPEndpoint getLocalEndpoint(int port,
RMIClientSocketFactory csf, RMIServerSocketFactory ssf) {
/*
* 查找将一个端点键映射到此客户端/服务器套接字工厂对的本地唯一端点列表(可能为null)的特定端口。
*/
TCPEndpoint ep = null;
// 使用localEndpoints对象对方法进行同步
synchronized (localEndpoints) {
// 创建一个 TCPEndpoint 实例作为端点键
TCPEndpoint endpointKey = new TCPEndpoint(null, port, csf, ssf);
// 获取本地唯一端点列表
LinkedList<TCPEndpoint> epList = localEndpoints.get(endpointKey);
// 获取本地主机名称
String localHost = resampleLocalHost();
// 如果本地唯一端点列表不存在
if (epList == null) {
/*
* 创建新的端点列表
*/
// 创建一个 TCPEndpoint 实例,作为新的唯一端点
ep = new TCPEndpoint(localHost, port, csf, ssf);
// 创建一个 LinkedList 实例,将新唯一端点添加到其中
epList = new LinkedList<TCPEndpoint>();
epList.add(ep);
// 设置唯一端点的监听端口和传输协议
ep.listenPort = port;
ep.transport = new TCPTransport(epList);
// 将端点键与新的唯一端点列表加入到 localEndpoints 对象中
localEndpoints.put(endpointKey, epList);
// 如果 tcpLog 的记录级别为 BRIEF,则记录端口创建成功的日志信息
if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) {
TCPTransport.tcpLog.log(Log.BRIEF,
"created local endpoint for socket factory " + ssf +
" on port " + port);
}
} else {
synchronized (epList) {
// 获取本地唯一端点列表的最后一个唯一端点
ep = epList.getLast();
// 获取最后一个唯一端点的主机名称、端口号和传输协议
String lastHost = ep.host;
int lastPort = ep.port;
TCPTransport lastTransport = ep.transport;
// 如果本地主机名称不为 null 且不等于最后一个唯一端点的主机名称
if (localHost != null && !localHost.equals(lastHost)) {
/*
* 主机名称已更新;将更新的端点添加到列表中
*/
// 如果最后一个唯一端点的端口号已设置,则移除旧唯一端点
if (lastPort != 0) {
epList.clear();
}
// 创建一个 TCPEndpoint 实例,作为新的唯一端点
ep = new TCPEndpoint(localHost, lastPort, csf, ssf);
// 设置唯一端点的监听端口和传输协议
ep.listenPort = port;
ep.transport = lastTransport;
// 将新唯一端点添加到列表中
epList.add(ep);
}
}
}
}
return ep;
}
这段代码有点难读,但是最后返回的就是一个TCPEndPoint实例
这个TCPEndPoint显然就是一个和网络请求有关的类
里面的属性有端口,ip之类的信息,其中的TCPTransport是真正的处理网络请求的类,这里实际上将TCPEndPoint和TCPTransport进行了绑定。
返回了一个ep变量
所以LiveRef里面就是放了一个TCPEndPoint。那么LiveRef可以理解为一个封装了ip、端口和一些id信息之类的辅助类。
现在LiveRef已经生成了
重新回到UnicastServerRef的构造函数
public UnicastServerRef(int port) {
super(new LiveRef(port));
}
这里调用了父类UnicastRef的构造函数
这里只有一个赋值操作
这时ref里存的就是原本在liveRef里的一些信息
记住这个ref,在之后我们还会用到
赋值操作结束后,又重新回到了UnicastRemoteObject类的exportObject方法
上面的这些过程中,LiveRef、TCPEndoPint、TCPTransport是处理网络通信的类。UnicastRef、UnicastServerRef是远程对象的引用对象,是宏观上处理网络请求的类,也是实现RMI调用的核心逻辑的类。UnicastRemoteObject就是远程对象的基础类,它并不直接处理网络请求,而是通过里面的UnicastServerRef处理。
目前其实就创建了一个UnicastServerRef,它的ref是一个LiveRef,ref里面有一个TCPEndpoint叫ep,ep里面有个TCPTransport叫transport。
继续跟进UnicastRemoteObject#exportObject(Remote obj, UnicastServerRef sref)
这里把sref,也就是包含着那一堆信息的内容给了一个UnicastRemoteObject实例的ref属性,其实是它父类的RemoteObject的ref属性
而这个这个实例应该是我们写的继承了UnicastRemoteObject的实现类
接下来调用了UnicastServerRef的exportObject方法,把刚才的这个实例传进去了,
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
Class<?> implClass = impl.getClass();
Remote stub;
try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;
}
可以看到在这个类里出现了stub,也就是最后客户端需要的存根
然后在这里很显然的创建了代理
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
这里的参数分别是我们最开始定义的远程对象类,UnicastRef实例对象(里面放着那个一堆信息的LiveRef),以及一个判断是否要强制创建stub的boolen对象
这里通过UnicastServerRef里的getClientRef()方法获取UnicastRef实例对象、
这里传入的ref其实就是UnicastRef里的ref,因为UnicastRef是UnicastServerRef父类,所以这里的ref和UnicastServerRef里封装的ref其实是同一个
然后接下来有个判断,判断是否创建存根
由于forceStubUse和ignoreStubClasses都是假,所以只要stubClassExists(remoteClass)是真就可以直接到createStub里
但是这里需要判断以我们的远程对象类的类名+_Stub为名字的类是否存在,存在才会成为true,这里我们不满足这个,所以进不了这个if判断
然后再往下,创建了一个动态代理。
这里就是给我们的远程调用类进行代理的
Invocationhandler里把UnicastRef实例对象放进去了
这里最核心的就是里边的ref,其实是同一个
毕竟这个代理对象就是代表远程对象的。
所以远程对象的Stub其实就是个动态代理,那么远程调用方法时就是调它的invoke方法,后续调用到的时候再分析。
stub是要传给客户端使用的,客户端通过操作stub的UnicastRef,来调用服务端远程对象的UnicastServerRef,是一个对称关系。
接下来判断这个Stub是否属于RemoteStub,如果是就调用setSkeleton。
RemoteStub是jdk里内置的几个通用类,有
Activation$ActivationSystemImpl_Stub
ActivationGroup_Stub
DGCImpl_Stub
RMIConnectionImpl_Stub
RMIServerImpl_Stub
ReferenceWrapper_Stub
RegistryImpl_Stub
接下来又创建了一个Target类
Target target = new Target(impl, this, stub, ref.getObjID(), permanent);
public Target(Remote impl, Dispatcher disp, Remote stub, ObjID id, boolean permanent)
{
// 初始化 weakImpl 为一个 WeakRef 对象,它持有传入的 impl 对象,并将其添加到 ObjectTable 的 reapQueue 队列中。
this.weakImpl = new WeakRef(impl, ObjectTable.reapQueue);
// 将传入的 disp 赋值给实例变量 disp。
this.disp = disp;
// 将传入的 stub 赋值给实例变量 stub。
this.stub = stub;
// 将传入的 id 赋值给实例变量 id。
this.id = id;
// 使用 AccessController 获取当前线程的上下文。
this.acc = AccessController.getContext();
// 获取当前线程的上下文类加载器。
ClassLoader threadContextLoader = Thread.currentThread().getContextClassLoader();
// 获取传入的 impl 对象的类加载器。
ClassLoader serverLoader = impl.getClass().getClassLoader();
// 检查当前线程的上下文类加载器是否是传入的 impl 对象的类加载器的子级。
if (checkLoaderAncestry(threadContextLoader, serverLoader)) {
// 如果是,则使用当前线程的上下文类加载器作为上下文类加载器。
this.ccl = threadContextLoader;
} else {
// 如果不是,则使用传入的 impl 对象的类加载器作为上下文类加载器。
this.ccl = serverLoader;
}
// 将传入的 permanent 赋值给实例变量 permanent。
this.permanent = permanent;
// 如果 permanent 为 true,则调用 pinImpl 方法将 impl 对象钉住(防止被 GC 回收)。
if (permanent) {
pinImpl();
}
}
Target可以理解为是一个远程服务实例,一个Target对应一个远程对象。里面保存了远程对象实例、对应的Stub、对应的远程引用对象UnicastServerRef,之前创建的LiveRef的ObjID。实际上Target就是用LiveRef的ObjID代表了这个远程对象,一个远程对象和一个LiveRef是绑定的。这里就包括了远程调用所需的全部对象了,远程调用就是Stub使用远程引用来调用远程对象上的方法。
创建完Target后,又调用了LiveRef的exportObject方法
ref.exportObject(target);
里面又调用了TCPEndPoint的exportObject
然后又是TCPTransport的exportObject
public void exportObject(Target target) throws RemoteException {
synchronized (this) {
listen();
exportCount++;
}
boolean ok = false;
try {
super.exportObject(target);
ok = true;
} finally {
if (!ok) {
synchronized (this) {
decrementExportCount();
}
}
}
}
listen方法里主要是监听一个端口
private void listen() throws RemoteException {
assert Thread.holdsLock(this); // 断言当前线程已经拥有锁,如果不满足会抛出 AssertionError
TCPEndpoint ep = getEndpoint(); // 获取 TCPEndpoint 对象
int port = ep.getPort(); // 获取 TCPEndpoint 对象的端口号
if (server == null) { // 如果 Server 对象为 null
if (tcpLog.isLoggable(Log.BRIEF)) { // 如果 TCP 日志记录器支持 brief 日志级别
tcpLog.log(Log.BRIEF,
"(port " + port + ") create server socket"); // 记录 brief 日志
}
try {
server = ep.newServerSocket(); // 创建 ServerSocket 对象
Thread t = AccessController.doPrivileged(
new NewThreadAction(new AcceptLoop(server),
"TCP Accept-" + port, true)); // 创建新线程,启动 AcceptLoop
t.start(); // 启动新线程
} catch (java.net.BindException e) { // 如果端口已被占用,抛出 ExportException 异常
throw new ExportException("Port already in use: " + port, e);
} catch (IOException e) { // 如果监听失败,抛出 ExportException 异常
throw new ExportException("Listen failed on port: " + port, e);
}
} else { // 如果 Server 对象不为 null
// otherwise verify security access to existing server socket
SecurityManager sm = System.getSecurityManager(); // 获取安全管理器
if (sm != null) { // 如果安全管理器不为 null
sm.checkListen(port); // 验证监听端口是否被授权访问
}
}
}
首先获取了之前保存的TCPEndpoint和端口,然后调用了TCPEndpoint.newServerSocket,跟进去实际最后就是创建了个普通的ServerSocket。然后创建了一个新的AcceptLoop类的监听线程并开启。然后就是等待客户端的连接了。
那么实际上服务就已经发布出去了。最后还有一步记录已经发布的Target,调用了TransPort的exportObject
public void exportObject(Target target) throws RemoteException {
target.setExportedTransport(this);
ObjectTable.putTarget(target);
}
在Target类里的setExportedTransport方法只是个简单的赋值操作,对应的TCPTransport保存进Target
void setExportedTransport(Transport exportedTransport) {
if (this.exportedTransport == null) {
this.exportedTransport = exportedTransport;
}
}
然后调用ObjectTable类putTarget方法
static void putTarget(Target target) throws ExportException {
ObjectEndpoint oe = target.getObjectEndpoint();
WeakRef weakImpl = target.getWeakImpl();
if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + oe);
}
synchronized (tableLock) {
if (target.getImpl() != null) {
if (objTable.containsKey(oe)) {
throw new ExportException(
"internal error: ObjID already in use");
} else if (implTable.containsKey(weakImpl)) {
throw new ExportException("object already exported");
}
objTable.put(oe, target);
implTable.put(weakImpl, target);
if (!target.isPermanent()) {
incrementKeepAliveCount();
}
}
}
}
putTarget是把对象和target绑定,放进ObjectTable这个类的静态变量objTable里面。
下面这段代码因为获取了DGCImpl.dgcLog变量,触发了DGCImpl类的实例化。这是垃圾回收相关的类。
if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + oe);
}
然后就会调用里面的static代码块
static {
/*
* 在与任意当前线程上下文隔离的上下文中“导出”单例的DGCImpl。
*/
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ClassLoader savedCcl = Thread.currentThread().getContextClassLoader();
try {
// 设置上下文类加载器为系统类加载器
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
/*
* 将远程收集器对象手动放入表格中,以防止在端口上侦听。
* (UnicastServerRef.exportObject将导致传输侦听。)
*/
try {
// 创建并导出 DGCImpl
dgc = new DGCImpl();
ObjID dgcID = new ObjID(ObjID.DGC_ID);
LiveRef ref = new LiveRef(dgcID, 0);
UnicastServerRef disp = new UnicastServerRef(ref);
Remote stub = Util.createProxy(DGCImpl.class, new UnicastRef(ref), true);
disp.setSkeleton(dgc);
// 创建一个Target对象,并将其放入ObjectTable中
Permissions perms = new Permissions();
perms.add(new SocketPermission("*", "accept,resolve"));
ProtectionDomain[] pd = { new ProtectionDomain(null, perms) };
AccessControlContext acceptAcc = new AccessControlContext(pd);
Target target = AccessController.doPrivileged(new PrivilegedAction<Target>() {
public Target run() {
return new Target(dgc, disp, stub, dgcID, true);
}
}, acceptAcc);
ObjectTable.putTarget(target);
} catch (RemoteException e) {
throw new Error("exception initializing server-side DGC", e);
}
} finally {
// 恢复上下文类加载器
Thread.currentThread().setContextClassLoader(savedCcl);
}
return null;
}
});
}
使用单例模式创建了一个DGCImpl对象,这个对象就是RMI的分布式垃圾处理对象,一旦有远程对象被创建,就会实例化这个对象,但也只会创建这一次。后面的代码和UnicastServerRef#exportObject里很像,创建一个代理。但这里和前面不同的是这是一个系统内置类,所以是直接创建了DGCImpl_Stub类,而不是创建的动态代理。
并且设置了disp的skeleton是DGCImpl_Skel。
disp.setSkeleton(dgc);
最后同样把这些放进Target,把Target保存进ObjectTable。
然后再把target保存到一个map表里,target里存放着一些信息
到这里服务发布就结束了。
最后在UnicastServerRef类的exportObject里return了一个stub,但代码里并没有接收。
第一步的创建远程对象的流程如图:
去掉垃圾回收的部分,创建远程对象本身并没有什么可以进行漏洞利用的部分,虽然步骤很繁琐,但实际上这一步基本上都是对ref进行一个又一个的封装。主要就是要把这个远程对象发布出去,然后利用动态代理创建一个stub,让之后的客户端可以通过stub来调用现在服务端发布的远程对象
创建注册中心
接下来我们来看服务端的第二步
创建注册中心
Registry registry = LocateRegistry.createRegistry(1099);
首先是调用了LocateRegistry的createRegistry方法,参数就是rmi注册中心的端口1099
然后在RegistryImpl的构造方法里
由于不满足这个if判断的后半段
会进入else里然后创建LiveRef实例,再把这个实例塞进UnicastServerRef里,这里虽然和第一步的时候的参数不太一样,但其实经过一堆super之类的方法,最后调的构造方法都一样
然后就进了setup
这里边就一个赋值,然后再去调用UnicastServerRef的exportObject方法
这一幕在创建远程对象里也出现过
但是这次的参数明显有不同,最主要的就是第一个参数的类不同了,而且第三个参数由false变为了true
然后我们跟进到它进行动态代理的类里
在创建远程对象时,这个if判断我们没能进入,主要就是因为stubClassExists里判断我们没能为真
但是现在remoteClass是RegistryImpl类,而RegistryImpl_Stub是存在的
所以现在我们能进入if语句中了
跟进createStub里
这里就是利用反射的方式去实例化RegistryImpl_Stub 类,然后return回来
RegistryImpl_Stub 继承了 RemoteStub ,实现了 Registry。这个类实现了 bind/list/lookup/rebind/unbind 等 Registry 定义的方法,全部是通过序列化和反序列化来实现的。
然后执行完毕后跳出方法回到UnicastServerRef的exportObject方法里继续往下执行
所以此时的stub就是一个实例化了的RegistryImpl_Stub类
里边套着LiveRef
因为此时stub就是一个实例化了的RegistryImpl_Stub类,而RegistryImpl_Stub继承了RemoteStub
所以接下来程序要执行setSkeleton函数了,看这个名字就知道,这个是创建Skeleton的
根据最开始对rmi的介绍,我们知道在客户端有stub,而在服务端就对应有Skeleton。客户端通过stub请求远程方法,服务端就通过Skeleton去调用方法,然后通过Skeleton获取结果,最后传给客户端Stub,客户端就从Stub获取结果
跟进setSkeleton
进到createSkeleton
通过反射对RegistryImpl_Skel进行实例化,和获取RegistryImpl_Stub的步骤类似
后边那就和创建和发布远程对象类似了
先创建个target对象把信息都放一块
这里因为disp接收的是UnicastServerRef类,而UnicastServerRef的skel在setSkeleton已经被赋值了,所以现在disp的skel属性也有值了
然后就是再进LiveRef的exportObject方法
然后一堆调用调到listen
这里也是和第一步不太一样的地方,因为第一步里调TCPEndpoint的getLocalEndpoint方法的时候
这里得到的epList的值是null,但现在这个不是null,也就进入了else里,给ep赋值了,有了这个transport属性
(至于为什么会这样我也不知道,跟着调没调出出原因,不过应该不是端口问题,我调试的时候改了端口,在第一步时的结果也是null
我是笨比,这个应该是有个变量在第一次执行的时候被改变了或者是缓存的原因,导致如果不结束idea的调试而是直接重置帧会导致epList不为空
绑定
注册说白了就是 bind 的过程,通常情况下,如果 Server 端和 Registry 在同一端,我们可以直接调用Registry 的 bind 方法进行绑定,具体实现在 RegistryImpl 的 bind 方法,就是将 Remote 对象和名称 String 放在成员变量 bindings 中,这是一个 Hashtable 对象。
先做个安全检查,然后再判断在hashtable中是否已经有了name这个键,如果没有就把name和object给put进hashtable里
客户端请求注册中心-客户端
接下来来分析一下客户端的部分
首先是请求注册中心Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
创建注册中心的时候是createRegistry,那请求的时候很自然就是get了
这里边就先判断端口,不存在就赋值成默认的1099端口,然后
再判断一下host
然后又是熟悉的在UnicastRef里套LiveRef
之后又是调用createProxy创建了一个代理对象,因为这个if成立,所以就直接进入了createStub里,不会去创建动态代理。
然后这一步就结束了。
里面的主要的过程和之前其实是一样的,就是创建并且返回了一个代理类
这个代理对象其实就是客户端的stub
之前我们说在创建远程对象的时候也有个stub,是UnicastServerRef返回回来的,但是代码里并没用去接收它
这里的stub也是createProxy创建的代理对象,所以其实是一样的东西(准确来说应该不太一样,直接通过createStub创建的是个代理对象,而不走createStub的创建的是个动态代理对象),只不过这里有接收它的东西
下一步,就是IRemoteObj remoteObj = (IRemoteObj) registry.lookup("iRemoteObj");
获取远程对象
Regsitry_Stub这里是class文件,没源码所以只能静态的跟了
首先是调用UnicastRef的newCall创建一个连接
接下来重点看var3.writeObject(var1);
它将lookup的参数进行了序列化的操作,这是客户端给注册中心传输的内容,那么如果注册中心想读取这个字符串,就会进行反序列化,这就有了造成反序列化漏洞的利用点
再看下面的super.ref.invoke(var2);
这里是调用的Unicastref里的invoke方法
这个invoke方法是stub里处理网络请求都要用到的方法
这里调用了call.executeCall()
,call这里就是我们之前利用newCall创建的连接,所以这里就是调用了StreamRemoteCall类的executeCall方法
这个函数里面有个处理异常的部分
可以看到这里也有个反序列化的地方
执行完invoke后,在lookup里还有一个被攻击的地方
这里是将注册中心返回的结果进行反序列化,所以假设我们现在有一个恶意的注册中心,就能攻击客户端
然后我们来整理一下客户端请求注册中心的过程中可利用的攻击客户端的攻击点
攻击点一
最明显的一个自然是在lookup里
将请求注册中心得到的内容进行反序列化的这一步
攻击点二
第二个地方是
lookup里调用Unicastref里的invoke方法
然后在invoke里又调用了StreamRemoteCall类的executeCall方法
在这个方法里先调用getInputStream获得注册中心的返回值
然后在后面的处理异常的部分,假设是TransportConstants.ExceptionalReturn这个异常,那么就会进行一个反序列化的操作
攻击点二相比攻击点一利用范围更广泛
因为攻击点一只有lookup和list方法才有,而攻击点二在lookup、bind、list、rebind、unbind这几个方法里都有
客户端请求服务端-客户端
在创建远程对象的时候我们提过
远程对象的Stub其实就是个动态代理,那么远程调用方法时就是调它的invoke方法,
stub是要传给客户端使用的,客户端通过操作stub的UnicastRef,来调用服务端远程对象的UnicastServerRef
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("iRemoteObj");
这行代码就相当于获得远程对象的stub
接下来String s = remoteObj.sayHello();
这一步就是去调用这个远程对象的stub
所以会先进RemoteObjectInvocationHandler的invoke里
这里有个invokeRemoteMethod(proxy, method, args);
,就是负责方法调用的
在这里面又调用了ref.invoke,这个ref在请求注册中心的第一步里已经赋值了
所以这里相当于调了UnicastRef的invoke方法
在这里调用了一个marshalValue方法,是判断参数类型的,但是我们这里没参数,所以连if判断都进不去(
判断完出来之后调了个call.executeCall();这里和请求注册中心的时候的攻击点一样
然后判断返回值是不是空,不为空的话就把结果放进in里,然后调用unmarshalValue
在unmarshalValue里也会判断类型,假如不是java的原始类型(就里边嵌的if里的那几种),那么就会到else里,对获得的in进行反序列化
这里是String,是引用类型,所以不满足判断,进行反序列化
这里就是服务端攻击客户端的攻击点
攻击点
这部分也是有两个攻击点,一个就是UnicastRef里调用的call.executeCall()
另一个就上边说的在unmarshalValue里如果值不是原始类型,就会进入else里对服务端返回的内容进行反序列化
客户端请求服务端-注册中心
这里要先知道断点下在哪
之前我们说客户端操作stub,服务端操作skel,所以断点应该下在skel里边
但是我们还是需要知道对应的位置
在创建注册中心的时候,我们调用了listen方法进行的网络监听
这里是开启了一个新线程
那监听肯定就是在这个线程完成的事,我们看里面的run方法
里面只有一个方法,再跟进去
这里又创建了一个线程池,跟进到ConnectionHandler的run方法
这个里边主要就是调用了run0
在run0里注册中心已经开始解析客户端传过来的一些东西了,但是重点在这
run0调用了handleMessages方法
这里会根据传过来的值做一些操作
默认情况下会调用serviceCall
serviceCall里就是取出我们之前存入的target,可以把断点下在这里
可以看到程序断在这了
然后程序会获取target里的disp
可以看到是同一个disp
然后再到这一步,调用disp.dispatch
这里就相当于调用UnicastServerRef类里的dispatch方法
skel不为空,进入oldDispatch里
在oldDispatch里又会调用skel.dispatch
终于进入到skel中了
这里边有个switch方法
0对应bind
1对应list
2对应lookup
3对应rebind
4对应unbind
这里我们因为客户端用了lookup方法,所以这里应该是进入case 2里
这里接收了客户端传过来的内容,之前说了客户端传的时候调用了writeObject,也就是序列化的形式传的,那接收的时候肯定要调用readObject进行反序列化,因此这里有可能成为客户端攻击注册中心的攻击点
客户端请求服务端-服务端
断点的位置和上一步一样,因为服务端和注册中心在一起,他们又都要处理网络请求
同样要到UnicastServerRef的dispatch里,不同的是这是skel为空
所以它就会继续往下走,走到这里
这里就也是判断参数,然后如果存在,就进入umarsharValue
,这里就和客户端请求服务端的时候的客户端一样了。只不过那里是客户端反序列化服务端返回的内容,这里是服务端反序列化客户端传入的内容
在之后就是真正调用方法了
然后就是再把调用后的结果进行序列化,然后又传回客户端
客户端请求服务端-dgc
最后一个部分,关于dgc的流程
这里其实在第一部分创建远程对象的时候就写了一点,不过我已经忘干净了(
RMI 定义了一个 java.rmi.dgc.DGC
接口,提供了两个方法 dirty
和 clean
:
- 客户端想要使用服务端上的远程引用,使用
dirty
方法来注册一个。同时这还跟租房子一样,过段时间继续用的话还要再调用一次来续租。 - 客户端不使用的时候,需要调用
clean
方法来清楚这个远程引用。
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。
总之dgc的具体作用就是垃圾回收的,然后他也有他对应的DGCImpl_Skel 和 DGCImpl_Stub
然后调用的话也是会到UnicastServerRef的dispatch里
再到UnicastServerRef的oldDispatch,然后执行DGCImpl_Skel的dispatch方法
这里有反序列化点
然后再说一下DGCImpl_Stub
在客户端上会调用DGCImpl_Stub
客户端lookup也会产生DGC通讯。(其实大多操作都会有DGC
在DGCImpl_Stub有clean和dirty两个方法,简单来看就都是垃圾回收的作用
而这两个方法里都调用了invoke
这就和客户端请求注册中心的攻击点二是一样的了
然后还有一点就是DGCImpl_Skel 里的switch的两个分支代表的就是DGCImpl_Stub的dirty和clean方法
总结
这篇主要就讲一下rmi整个的流程,虽然还有一些地方不太清楚,但感觉写的还凑合吧,至于利用的部分还是再开一篇吧。
坦白讲目前我也不太清楚rmi的攻击是怎么实现的,因为我感觉这里如果要伪造rmi客户端进行攻击,那首先就需要知道服务端定义的远程对象的接口形式才能去传参。更何况这个远程对象还是绑定在本地的,不太清楚能不能绑到远程。而如果伪造服务端,那被攻击的客户端需要恰好调用这个伪造的服务端的方法,从直觉上来说应该不会有一个客户端去故意请求一个攻击者所在的服务器的方法吧。也许是在已经拿下rmi服务端的时候利用(?
参考链接
https://su18.org/post/rmi-attack/