Java RMI千字分析

xiao1star2025-12-26文章来源:SecHub网络安全社区


RMI概念

Java RMI 是一种允许一个 Java 虚拟机(JVM)中的对象调用另一个 JVM 中对象的方法的技术。它是一种基于 Java 的分布式计算技术,使得开发者可以轻松地实现跨网络的远程方法调用。RMI分为一个Server端和一个Client端,其实还有一个注册中心,一般情况注册中心的创建都是在Server端

20251103171915124.png

**Client端:**客户端通过注册中心查找远程对象的引用(Stub)。客户端需要知道注册中心的地址和远程对象的名称。

**Server端:**服务器端将远程对象实例注册到注册中心,使得客户端可以通过注册中心找到并调用这些远程对象的方法;服务器端负责处理客户端的远程方法调用请求,并返回结果

**注册中心:**注册中心是一个特殊的 RMI 服务,用于存储远程对象的引用(Stub)。服务器端将远程对象注册到注册中心,注册中心会为每个远程对象分配一个唯一的名称。

三者之间的关系:注册中心是Client端与Server端的桥梁、Server端提供服务、Client端调用服务

RMI的代码实现

在正常的RMI机制中,是由两台不同的计算机实现的,但是资源有限这里是在一台主机上创建了两个项目分别是RMIServerRMIClient

RMIServer

在RMIServer项目中有

远程接口:继承Remote类、声明要调用的方法

远程接口实现类:继承UnicastRemoteObject类以及远程接口、实现远程接口中要调用的方法

RMI服务类:创建注册中心、实现远程对象的绑定

import java.rmi.Remote;
import java.rmi.RemoteException;

/*
* 远程接口,需要继承 java.rmi.Remote接口,并在接口中声明要暴露给远程调用的方法
*
* */
public interface RemoteItf extends Remote {
    String Hello(String name) throws RemoteException;
}

**注意:**这个Hello方法必须中的throws RemoteException不能省略,因为 Java RMI 中,远程接口中的方法必须声明抛出 RemoteException,这是由 RMI 的设计和工作原理决定的,若省略会有报错

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
/*
* 实现远程接口的类需要继承自 java.rmi.server.UnicastRemoteObject
* 并继承远程接口类实现接口中定义的方法。
*
* */
public class RemoteImpl extends UnicastRemoteObject implements RemoteItf {
    public RemoteImpl() throws RemoteException{

    }
    @Override
    public String Hello(String name){
        String upperCase = name.toUpperCase();
        System.out.println(upperCase);
        return upperCase;
    }

}
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
/*
* 通过java.rmi.registry.LocateRegistry 将远程对象绑定到 RMI 注册表中。
* */
public class RMIServer {
    public static void main(String[] args) throws RemoteException {
        RemoteItf remoteItf = new RemoteImpl();
        Registry registry = LocateRegistry.createRegistry(1099);//创建注册中心、端口是1099
        registry.bind("remoteItf", remoteItf);//远程对象的绑定
    }
}

RMIClient

在RMIClient项目中有

远程接口:与RMIServer的远程调用接口一模一样,为了防止获取到远程对象之后没有这个类而报错

RMI客户端类:用于获取注册表中的远程对象并调用远程对象

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteItf extends Remote {
    String Hello(String name) throws RemoteException;
}
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
/*
* 通过 RMI 注册表查找远程对象的引用.并通过代理对象调用远程方法。
* */
public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);//获取注册中心
        RemoteItf remoteItf= (RemoteItf) registry.lookup("remoteItf");//通过注册中心查找远程对象
        String rmi = remoteItf.Hello("rmi");
        System.out.println(rmi);
    }
}

代码运行

首先启动服务端的项目,实现注册中心的创建以及远程对象的绑定

20251103171926391.png

之后启动客户端的项目,实现从远程对象的获取以及方法的调用

20251103171937045.png

之后再服务器端也成功的回显了RMI字符串

20251103171940178.png

RMI运作流程代码分析

20251103171943648.png

还是先看这个图片:

RMI中的注册中心(Locate Registry)是一个hash表,里面是名字以及其对应的远程对象,在图中可以看出客户端不是直接调用服务端的,而是通过代理,其中客户端的代理是Stub、服务端的代理是Skeleton。有这些代理的原因是因为像服务端的内容是不会想要实现网络请求中的东西,而这两个代理就是将网络请求的内容封装起来

还可以看到整个RMI的通信是由6条:

服务端–>注册中心;注册中心–>服务端

客户端–>注册中心;注册中心–>客户端

客户端和服务端通过JRMP协议相互通信

Server端创建分析

Server端远程对象创建分析

我们的分析这个目的就看看如何将一个远程对象发布到网上去

RemoteItf remoteItf = new RemoteImpl();打上断点

20251103171947753.png

下一步来到RemoteImpl类中,调用其类中的构造方法,要注意该该类中继承了UnicastRemoteObject

20251103171951161.png

接着就来到了UnicastRemoteObject类的构造方法,注意这里我也是打了一个断点,因为我这里不打断点根本进不来,发现这个方法直接又调用了其类中的一个有参的构造方法其中的port参数传入的值为0

20251103171954725.png

接着就来到UnicostRemoteObject类中的一个有参的构造方法,再往后的操作就是将远程对象发布到一个随机的默认端口,其实在RMI中的默认端口是注册中心的端口,但是我们现在分析的是远程对象并非注册中心的,这是给的一个随机的端口

20251103171958544.png

来到UnicostRemoteObject中的exportObject方法中,这个方法似乎是一个导出方法,也是一个静态方法,这个exportObject方法前一个参数是我们的远程对象,后一个参数是调用了创建了一个UnicastServerRef对象,而这个UnicastServerRef类看名字就是一个服务端引用类,专业点说就是专门用于在远程对象与客户端之间进行通信时提供服务器端的引用

20251103172004331.png

若我们的远程对象没有继承UnicostRemoteObject类,那就需要我们手动调用这个静态方法

20251103172007829.png

在往下就来就看看这个UnicastServerRef类的构造方法,听名字就是像一个服务端引用,这个构造方法又调用了其父类的构造方法,传入了一个LiveRef对象

20251103172011138.png

接着看LiveRef类的构造方法,又是一个构造方法套构造方法,再第二个构造方法中我们发现又套了一个构造方法,其中第二个参数还调用了TCPEndpointgetLocalEndPoint方法

20251103172014335.png

接着就来到TCPEndpint类中,我们查看他的构造方法,发现是获取了host和port,可以想到这也是一个处理网络请求的封装类

20251103172017905.png

获取到网络请求的相关内容后就将其结果赋值给ep,将其都封装在LiveRef类中

20251103172020823.png

接着就来到TCPEndpintgetLocalEnpoint方法,将默认值port传入其中getLocalEnpoint

20251103172023490.png

接着来到TCPEndpointgetLocalEndpoint方法中,该方法比较长

public static TCPEndpoint getLocalEndpoint(int port,
                                           RMIClientSocketFactory csf,
                                           RMIServerSocketFactory ssf)
{
    /*
     * Find mapping for an endpoint key to the list of local unique
     * endpoints for this client/server socket factory pair (perhaps
     * null) for the specific port.
     */
    TCPEndpoint ep = null;

    synchronized (localEndpoints) {
        TCPEndpoint endpointKey = new TCPEndpoint(null, port, csf, ssf);//创建了一个新的TCPEndpoint对象
        LinkedList<TCPEndpoint> epList = localEndpoints.get(endpointKey);//查看localEndpoints中是否存在键为endpointKey的键值对
        String localHost = resampleLocalHost();//获取本地localhost

        if (epList == null) {//这肯定是为空的,因为我们就没有对epList进行任何操作
            /*
             * Create new endpoint list.
             */
            ep = new TCPEndpoint(localHost, port, csf, ssf);//创建了一个新的TCPEndpoint对象赋值给ep
            epList = new LinkedList<TCPEndpoint>();
            epList.add(ep);//将得到ep赋值给epList列表中

这是此时ep中的信息

20251103172027082.png

将ep添加到epList中后eplist中的信息

20251103172029932.png

ep.listenPort = port;
ep.transport = new TCPTransport(epList);//又将eplist中的内容作为参数创建一个TCPtTransport对象赋值给ep的transport成员变量
localEndpoints.put(endpointKey, epList);

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;
        // assert (localHost == null ^ lastHost != null)
        if (localHost != null && !localHost.equals(lastHost)) {
            /*
                     * Hostname has been updated; add updated endpoint
                     * to list.
                     */
            if (lastPort != 0) {
                /*
                         * Remove outdated endpoints only if the
                         * port has already been set on those endpoints.
                         */
                epList.clear();
            }
            ep = new TCPEndpoint(localHost, lastPort, csf, ssf);
            ep.listenPort = port;
            ep.transport = lastTransport;
            epList.add(ep);
        }
    }
}
}

return ep;
}

eptransport值中的内容

20251103172036391.png

反正这个方法就是将host和prot赋值给ep然后将ep返回

接着就来到LiveRef的构造方法,将objID以及前面的episLocal进行赋值操作

20251103172039727.png

可以看到这是将前面获取的Endpoint对象封装到了LiveRef当中,也就是LiveRef@616

20251103172043454.png

接着来到还是回到UnicastServerRef的构造方法,这又调用了其父类的构造方法UnicastRef

20251103172047346.png

可以看到其父类是UnicastRef这是客户端的引用,大家可能会疑惑为什么客户端的引用要放在服务端创建呢,后面会有解释

20251103172051049.png

可以看到这个liveRef就是我们之前获取到的Endpoint对象中的内容,将其又封装到了LiveRef中的LiveRef@616

20251103172055899.png

上述内容就是创建一个UnicastServerRef对象,接下了我们查看UnicastRemoteObject类中的exportObject方法

20251103172100522.png

接下来来到了UnicastRemoteObjectexportObject方法中,该方法看名字类似一个导出类,可以发现该方法首先是判断传入的第一个参数是否是UnicastRemoteObject子类,最终是调用sref.exportObject方法

20251103172110428.png

要注意RemoteImpl继承的就是UnicastRemoteObject类,所以需要进入到if语句中,若不继承还是想要实现RMI服务端,就需要直接调用UnicastRemoteObjectexportObject方法

20251103172113294.png

接着就来到UnicastServerRefexportObject方法中该方法调用了Util的createProxy方法用于实现动态代理,将结果赋值给stub对象,接着创建了Target对象,然后调用LiveRefexportObject方法,最终返回stub的结果

20251103172117591.png

思考:为什么客户端的代理stub要在服务端创建呢

回答:这是这个stub需要在服务端创建好之后,接着放到注册中心上,然后客户端去注册中心上获取,进而与服务端的代理skeleton连接,进而获取到服务端的远程对象

先来到UtilcreatProxy方法中,这是实现了一个动态代理

20251103172120995.png

里面的handler参数中的clientRef就是我们之前得到的LiveRef@616

20251103172124625.png

如下,这是一个if判断

20251103172127737.png

这个stubClassExits用于判断是否有与remoteClass参数的值与_Stub而成的类名,要直接到这个remoteClass的值是实现远程接口类RemoteImpl,这是我们自己创建的一个类,肯定是没有的,结果为false;forceStubUse也为false,ignoreStubClasses的结果也为false。最终结果不会走到if语句中

20251103172131576.png

只要调用的是如下类似于这种函数stubClassExits的结果才能返回为true

20251103172134237.png

最后成功创建了一个动态代理将其赋值给了stub,如下可以看到stub所包含的内容,还是将LiveRef@616封装在了里面

20251103172137463.png

接着来到Target的对象创建的分析,我们可以理解为将我们之前所创建的东西又封装到了Target,如下图所示可以看出disp(UnicastServerRef)和stub(UnicastRef)分别代表服务端和客户端,但是里面的内容都是一致的,都是LiveRef@616

20251103172140303.png

创建完Target对象后,又调用了ref的exportObject方法将target发布了出去

20251103172143871.png

来到LiveRefexportObject中发现其又调用了ep.exportObject方法

20251103172147033.png

20251103172150329.png

逐步追踪,最后来到了TCPTransport方法中,先是会调用listen方法,之后又调用了其父类的exportObject方法

20251103172153005.png

首先看listen方法,主要作用是监听一个TCP端口,以便接受客户端的连接请求。

20251103172155460.png

来到newServerSocket方法中,该方法通常用于RMI服务端的实现,用于创建一个监听特定端口的服务器套接字。在该方法中若listenport的值为0,那么就会调用setDefualtPort方法随机生成一个端口

20251103172158409.png

走完listen方法,服务端就已经把远程对象发布出去了,且发布在了一个随机的端口上,用户是不知道的

分析完listen方法我们来来到其父类的exportObject方法中,这个方法主要用于记录之前的内容

20251103172202616.png

20251103172201116.png

分析到这里整个Server端远程对象创建的过程就分析完了

注册中心的创建

在如下图所示LocateRegistry.createRegistry(1099);处,打上断点。这里传了一个port参数值为1099,用于后续注册中心开放的端口

20251103172205376.png

来到LocateRegistry类下的createRegistry方法中,在这里面新建了一个RegistryImpl对象并将值为1099的port参数传入其中

20251103172208947.png

来到RegistryImpl方法中,该方法首先是进行一个if判断,这是用于安全检验,不会进入到if语句中,这里就不多说了,会直接来到else语句中

20251103172212555.png

先看new LiveRef(id, port),是不是感到十分熟悉,这在前面Server端远程对象创建中提到过,这里就不多说了

20251103172214959.png

最终创建的LiveRef对象lref如下图所示

20251103172216588.png

接着来到setup(new UnicastServerRef(lref));中的new UnicastServerRef(lref)中,这也十分熟悉

20251103172224054.png

如下图所示是refliveRef的值都是LiveRef@889

20251103172221364.png

接着来到RegistryImplsetup方法中,接着调用了UnicastServerRefexportObject方法,在第一个参数是RegistryIpml,第二个参数是null,三个参数是true

20251103172226817.png

接着来到exportObject方法中,这也十分熟悉,之前分析过,这里就不多说了,但是与之前唯一的区别就是第三个参数parmanet的值是true, 表示这个代理的创建是永久的,我们直接看createProxy这个创建动态代理方法

1738409086998a82db0a513fb4e9695131171cc48035d.png

来到createProxy方法中,这里与之前的远程对象的创建不同,远程对象创建是没有进入到if语句中,而是通过利用newProxyInstance来实现动态代理,而在这里直接进入了if语句中调用了createStub方法

20251103172229870.png

为什么会进入到if语句中呢,只是因为我们要代理的接口是java中自带的类RegistryImpl,其存在后接stub的类名,所以进入了if语句中

20251103172231376.png

来到createStub方法中

20251103172235538.png

最终的到的stub的值是一个RegistryImpl_Stub对象

20251103172234006.png

这个RegistryImpl_Stub类继承了RemoteStub

20251103172237929.png

因为上图的解释,所以调用了setSkeleton

20251103172241459.png

接着来到setSkeleton方法中,在这个方法里面调用了createSkeleton方法

20251103172244403.png

这是获取RegistryImpl_Skel的class对象利用反射来创建skeleton代理对象

20251103172246019.png

接着就来到后面,首先是将前面的创建的东西封装到target中,之后再调用exportObkect方法

20251103172247671.png

target中的内容

20251103172249336.png

接着来到exprot方法中,和前面一样,从LIveRef–>TCPEndpoint–>TCPTransport

1738463105292f98e7a4cd15a43c087c5f0c0f8f2c313.png

在TCPTransport的listen()方法中也是与之间分析的一模一样的,例如开启线程等操作,不做分析了,后面就会调用其父类的exportObject方法

20251103172250779.png

最后来到Transprot的exportObject方法中,这就是将最后的target对象,放到一个Object表中,记录下来

20251103172557855.png

可以看到ObjectTable的大小为三,按道理来说应该是两个,一个是Server远程对象创建时生成也就是,一个是创建注册中心生成的。我们点进去仔细看一下

20251103172253600.png

如下是第一个的内容,可以发现第一个的skel是由DGCImpl_Skel创建的,而stub是由DGCImpl_stub创建的,这两个类我们在向前分析都没有看到,其实这是一个分布式垃圾回收,是系统默认会创建的,这个也是十分重要的,我们后面再分析

20251103172255276.png

如下是第二个,可以看到这里的skel是为空的,而stub是通过RemoteObjectInvocationhandler类创建的,就是动态代理生成的,可以证明这个是前面Server端远程对象创建时生成的

20251103172258136.png

如下是第三个,在这里可以看到skel是由Registry_Skel创建的,而stub是由Registry_Stub创建而成的,这也就是创建注册中心时生成的

20251103172300082.png

到这里就把注册中心的创建分析完了

绑定分析

我们接下来分析registry.bind("remoteItf", remoteItf),这就是将所创建的远程对象与其对应的名字remoteItf进行绑定放入到一个Hashtable

20251103172302329.png

20251103172305321.png

若bings中不存在我们传入的值,那么就将其put到里面

20251103172306705.png

可以看到成功put到了里面

20251103172308351.png

总结

到这里Server端的创建分析全部完成,到目前为止我们都没有发现可以由漏洞利用的点,但是依旧进行大篇幅的分析是为了更好的了解Server端的运作机制

Client端创建分析

客户端的操作主要有两个操作:

1.向注册中心获取代理Stub

2.利用代理Stub向服务端做真正的远程调用

Client端请求注册中心

首先我们先分析LocateRegistry.getRegistry("127.0.0.1",1099),在这里我们给上了本地地址以及端口1099,用于去寻找Server端的注册中心,注意要运行Server端的代码

如下图打上断点

20251103172310621.png

接着来到了LocateRegistry getRegistry

20251103172312183.png

可以看到上面的是将是将hostport传递过去之后,创建了一个Registry对象,后面进行了对port和host进行了一系列的判断,也不会进入到if语句中

下面就来到了TCPEndpoint、LiveRef、以及UnicastRef,这些类我们前面都见过了,就是进行一些封装操作。之后调用Util.createProxy方法实现,这个和在服务端注册中心的创建的代码是一样的,传入的参数都是RegistryImpl的class对象

20251103172628041.png

来到createPrxoy中,这里接着会来到createStub方法,创建一个stub代理

20251103172314611.png

可以看到和向前的分析是一样的,也是利用反射来创建的

20251103172316023.png

最终走完所有的,得到的结果如下图所示

20251103172317559.png

总结:本以为Client端是通过注册中序序列化来远程获取服务端所创建的Stub,现在可以看到Client端是通过所给的参数porthost又在本地创建了一次Stub

接下来分析(RemoteItf) registry.lookup("remoteItf"),这是从注册中心获取到远程对象。我们知道实际上要调用RegistryImpl_Stublookup方法,但是这个用于本人的java版本问题导致只能看其.class文件,无法直接调试进入,我们就直接分析了

20251103172638972.png

直接来到RegistryImpl_Stublookup方法,可以看到这个是调用了newCall方法,用于创建一个连接,接着将我们传入的字符串放入到了一个writeObject输入流中,接着就调用一个invoke方法,这个方法是一个激活的方法

20251103172320015.png

来到invoke方法中,可以看到是调用了一个executeCall方法

20251103172704821.png

接下来就来到StramRemoteCallexecuteCall方法,这是一个客户端处理网络请求的方法

20251103172321864.png

在下面可以看到在这里对成员变量in调用readByte方法进行读字节操作赋值给returnType,接着下面对returnType进行了异常处理操作,当是TransportConstants.ExceptionalReturn异常时就会调用readObject方法

20251103172325450.png

可以看到inConnectionInputStream类型,但连接时有输入流进行对其进行赋值操作,那么可以想到如果注册中心返回的输入流是一个恶意的对象且正好触发了这个异常调用了readObject,进而实现了反序列攻击

20251103172719351.png

上述的漏洞利用范围很广,要知道触发这个反序列攻击是在invoke方法中,也就是说只要哪个方法调用了 super.ref.invoke方法都会触发这个反序列化漏洞。下图就是其他方法(list、rebind、unbind)调用这个方法的地方,而这几个方法都是在RegistryImpl_Stub类下,可以在起初低版本中RMI的漏洞点之多

20251103172724764.png

20251103172328002.png

20251103172326774.png

接着完成这个invoke请求之后,由获取了以一个输入流var2,这个var2其实也就是我们与注册中心建立连接之后注册中心返回给我们的内容,然后调用readObject方法反序列化将其读取了出来,可以看到如果我们有一个恶意的注册中心的话就可以实现反序列化攻击

20251103172333058.png

总结:如上就是Client端请求注册中心的主要内容,漏洞的利用主要是在lookup获取远程对象的方法中存在了两个反序列化漏洞点,一个是RegistryImpl_Stub类的lookup方法中的,一个是StramRemoteCallexecuteCall方法中的

Client端请求服务端

接下来就是获取完远程对象调用方法的分析即remoteItf.Hello("rmi")

20251103172331300.png

可以看到前面得到remoteItfRemoteObjectInvocationHandler类,也就是一个动态代理类

20251103172337960.png

要知道调用一个动态代理的方法,首先会调用它重写的invoke方法,因此首先会先调用RemoteObjectInvocationHandlerinvoke方法,前面是进行了一些if判断用于异常处理就不多说了,最后会调用invokeRemoteMethod方法

20251103172341214.png

接着发现又调用了另一个invoke方法

20251103172345526.png

接着就来到UnicastRefinvoke方法,

20251103172352784.png

再往下来到了marshalValue方法中,在这个方法中会对我们的value值进行writeObject序列化操作,传递给服务端,可以看到下面第三个图也就是对我们传入的参数rmi进行序列化操作

20251103172347360.png

20251103172348840.png

20251103172359159.png

继续往下可以看到调用了StreamRemoteCallexecuteCall方法,只要是客户端的请求都会调用这个方法,我们知道这个方法里面是存在反序列化攻击的,这里就不多说了

20251103172401201.png

在往下看这里有一个unmarshalValue方法,如果in中有内容且不是if语句给的类型就会将其进行readObject方法,进行反序列化操作,这里是存在一个反序列化攻击点,而我们现如今呢这里是String类型会进行反序列化造作

20251103172403395.png

20251103172405338.png

最终调用完unmarshalValue赋值给returnValue得到rmi的转大写,完成远程对象函数的调用

20251103172407358.png

到这里就分析完了,与客户端请求注册中心一样这里也有两个反序列化的点,一个是unmarshalValue方法,我们客户端在调用远程对象时所传入的参数,在unmarshalValue方法中对这个从服务端返回过来的内容进行了ReadObject反序列化操作;一个是executecall方法,这个是Client端进行网络请求时会触发,和之前的触发反序列方式是一样的,在这里面触发的协议也就是JRMP协议

Client端启动后注册中心以及Server端分析

注册中心

分析Client端启动后注册中心的动作,其实也就是Client端调用lookup去寻找远程对象时注册端的操作

20251103172408918.png

可以想一下之前在Server端分析注册中心的创建时,是不是里面有一个方法是开启了一个线程,然后等待客户端连接处理网络请求的,接下来我们就着重分析一些这个里面:这是客户端请求连接,服务端(注册中心)接受这个请求之后所作的内容

我们看到下图在创建线程t时,在最里面的一个参数是创建了AcceptLoop对象,我们进去一看究竟

20251103172410921.png

要知道启动线程之后都会调用它的run方法,来到这里TCPTransport类下的run方法,在这里面会调用executeAcceptLoop方法

20251103172412241.png

接下来来到executeAcceptLoop方法中,该方法是接受连接以及通过线程池处理客户端连接,connectionThreadPool.execute(new ConnectionHandler(socket, clientHost));用于处理连接的,接着我们就来到ConnectionHandler类中

20251103172418592.png

来到ConnectionHandler类的run方法中,接着我们进入到下面的run0方法中

20251103172415378.png

来到run0方法中,这个方法很长但可以看到就是处理一些Http请求然后读取内容,这都不是我们的重点

20251103172744393.png

重要的还是handleMessages(conn, false)方法,用于读取输入流中的信息,在下图可以看到这个方法也是读取输入的字节流赋值给op,之后进行switch判断进行不同的操作

20251103172421507.png

正常默认情况下会来到这个Call中,可以看到这个里面就是创建了StreamRemoteCall对象,然后就调用了serviceCall方法进行if判断

20251103172423326.png

我们来到serviceCall方法中,在这里我们看到了是从我们之前创建的ObjectTable中获取了这个Taget,我们在这里打上断点,之后启动Client端

20251103172425294.png

可以看到target里面的内容,这不就是之前在服务端创建好的stub以及skeleton

20251103172428589.png

再往下就说获取target的dispatcher,可以看到就是UnicastSeverRef中的

20251103172432753.png

在往下就来到disp.dispatch方法中

20251103172434730.png

UnicastServerRefdispatch方法中,首先是获取了call的输入流然后读取,因为我们的skel是为RegistryImpl_Skel的,并不为空所以来到oldDispatch方法中

20251103172437283.png

oldDispatch方法中,也是想读取了输入流

20251103172440136.png

再往下就来到skel.dispatch方法中

20251103172442078.png

进入RegistryImpl_Skel类的dispatch方法中,这个类是.class文件,就不进行调试了,在这里可以看到是调用了switch语句根据var3的值进行不同的case操作

20251103172443836.png

我们最终是来到case 2中,这里获取完输入流之后直接进行了反序列化操作,然后下面就调用了lookup方法

20251103172447059.png

这里读取的输入流就是我们在Client端lookup参数,可以看到我们客户端就可以通过这个点攻击注册中心

20251103172449320.png

可以除了case2之外,case3case4都会有反序列化操作

20251103172452941.png

20251103172454381.png

到这分析完毕了

Server端

接下了分析Client端请求Server端时也就是String rmi = remoteItf.Hello("rmi"),Server端是如何处理的

20251103172456525.png

其实和前面注册中心的差不多,因为都会进行网络请求的操作,后面都来到了serviceCalldisp.dispatchh方法

20251103172458169.png

再到后面就不同了这里的skel是为null的,无法进入到if语句中进行,这里与注册中心的不一样

20251103172459502.png

就会接着往下来就会获取到我们的这个方法也就是远程对象中的Hello方法

20251103172501352.png

20251103172504567.png

再往下就会获取参数类型以及参数值,然后调用了我们十分熟悉的unmarshalValue方法

20251103172506213.png

在这里跳过一系列if判断就会来到readObject反序列化操作

20251103172508053.png

调用完这unmarshalValue方法,可以看到这个params数组中第一个值就是我们从客户端传过来的rmi

20251103172509528.png

20251103172511640.png

再往下就是通过invoke调用这个Hello方法,然后下将得到的结果通过marshalValue序列化传递给客户端

20251103172513226.png

到这里就结束了

DGC分析

前面在Server端创建注册中心时,发现ObjectTable表中,有一个由系统自己创建的 键值对(也就是DGC)

20251103172514974.png

那么是在哪里创建的呢,其实就是在putTarget方法中,在这个方法呢就是将我们的Target放到ObjectTable表中。在下图红框圈起来的if语句中,就有调用一个DCGImpl的方法,看着像是一个日志的写入,我们进去这个类中看一眼

20251103172516994.png

来到这个类中发现这个dgcLog是一个静态方法,要知道当调用一个类的静态成员变量或静态方法时,这个类就会被初始化但前提是该类尚未被初始化,也会调用这个类的静态代码块

20251103172519967.png

而正好DGCImpl方法中就有一个静态代码块,可以清楚的看到他和这个注册中心的创建是一样的,创建了stubskeleton,且最后封装到了Target中,又put到了ObjectTable表中,这就是为什么后来这个表中会有DGC的东西

20251103172522540.png

1738583809915a9ced7a8ac8546dc8a95ba6f87b6d2d8.png

**DGCImpl_Stub**中有两个方法一个是clean,一个是dirty

clean方法中有一个super.ref.invoke方法,前面也说过了这个里面是由反序列化漏洞的

20251103172802544.png

20251103172527136.png

20251103172529406.png

而在dirty方法中也是有的

20251103172531834.png

**DGCImpl_Skel**中也是存在反序列化漏洞的

20251103172533693.png

20251103172536719.png

漏洞利用总结

1、攻击客户端:
RegistryImpl_Stub#lookup->注册中心攻击客户端
DGCImpl_Stub#dirty->服务端攻击客户端
UnicastRef#invoke->服务端攻击客户端
StreamRemoteCall#executeCall->服务端/注册中心攻击客户端
2、攻击服务端
UnicastServerRef#dispatch->客户端攻击服务端
DGCImpl_Skel#dispatch->客户端攻击服务端
3、攻击注册中心
RegistryImpl_Skel#dispatch->客户端/服务端攻击注册中心