RMI学习(二)

RMI源码分析

 Von's Blog     2022-03-16   72598 words    & views

之前我们已经简要的学习过了RMI的总体流程,也提到了RMI中的数据传输都是通过序列化和反序列化实现的,因此其实有着很大的攻击面,不过之前我们还没有学习反序列化的流程,也就没有深入的去探讨RMI,这次就来详细的学习一下。

image-20220402200102134.png

不同于上次我们宏观的观察RMI的流程,实际上进行一个完整的服务注册、发现、调用流程分很多细分步骤,本篇先从源码的角度详细探究RMI的完整流程。

创建远程对象

上来的第一步就是最麻烦的一步,我们在前面利用RMIinterfaceimpl rmIinterfaceimpl = new RMIinterfaceimpl();创建了一个远程对象,其中RMIinterfaceimpl继承了UnicastRemoteObject。

public class RMIinterfaceimpl extends UnicastRemoteObject implements RMIinterface{
    RMIinterfaceimpl() throws RemoteException {
    }

    @Override
    public String test() throws RemoteException {
        System.out.println("Server:Hello!");
        return "Hello!";
    }
}

所以执行代码时首先会调用UnicastRemoteObject的无参构造方法:

protected UnicastRemoteObject() throws RemoteException
{
    this(0);
}

又调用了:

protected UnicastRemoteObject(int port) throws RemoteException
{
    this.port = port;
    exportObject((Remote) this, port);
}

给UnicastRemoteObject对象的port属性赋予了默认值0,当为0时,其实就是会在后续将远程服务开放在一个随机的端口(注意这里不是注册中心的1099端口,是远程服务的端口,这个port属性在未来我们还会用到)

而下面的exportObject方法呢,其实是一个静态方法:

public static Remote exportObject(Remote obj, int port)
  throws RemoteException
{
  return exportObject(obj, new UnicastServerRef(port));
}

他又调用了exportObject的另一个重载形式,并将obj(也就是RMIinterfaceimpl对象自己)和一个UnicastServerRef(记住这个类!)对象传入其中。

UnicastServerRef

来看下UnicastServerRef(port)是什么:

public UnicastServerRef(int port) {
    super(new LiveRef(port));
}

LibeRef

那继续进入这个LiveRef类看看:

public LiveRef(int port) {
  this((new ObjID()), port);
}

又调用了本类的另一个重载方法,第一个参数为objID(大致就是创建一个ID对象),第二个参数还是port,看下这另一个重载方法:

public LiveRef(ObjID objID, int port) {
  this(objID, TCPEndpoint.getLocalEndpoint(port), true);
}

这里又调用了本类的另一个构造方法重载,并传入了三个参数,其中第二个参数为TCPEndpoint.getLocalEndpoint(port),得到一个Endpoint对象,其实也就是java中处理TCP请求的对象了。这里我们就先不跟TCPEndpoint.getLocalEndpoint()了

来看继续下去其调用的另一个构造方法重载:

public LiveRef(ObjID objID, Endpoint endpoint, boolean isLocal) {
  ep = endpoint;
  id = objID;
  this.isLocal = isLocal;
}

这里终于执行了赋值,为LiveRef的三个属性赋值,本质上来说此时传参的内容为:

LiveRef(new ObjID(), TCPEndpoint.getLocalEndpoint(port), True)

归结到这里其实嵌套了三层LiveRef()的构造方法,为:

LiveRef(int port) --> LiveRef(ObjID objID, int port) --> LiveRef(ObjID objID, Endpoint endpoint, boolean isLocal)

到这里我们得到了一个LiveRef对象(请注意,这个对象后面还会频繁出现!,为了方便称呼,我们下文统称为R),其三个属性被赋值了:

ep = TCPEndpoint.getLocalEndpoint(port);
id = new ObjID();
this.isLocal = True;

回到之前UnicastServerRef类的构造方法,在得到一个LiveRef对象后他调用了其父类的构造方法:

public UnicastRef(LiveRef liveRef) {
    ref = liveRef;
}

也就是将之前的R赋值给了ref属性。请注意,到这里时我们得到了一个UnicastServerRef对象其ref属性为R.

exportObject

回到上面的:

public static Remote exportObject(Remote obj, int port)
  throws RemoteException
{
  return exportObject(obj, new UnicastServerRef(port));
}

其调用了exportObject()的另一个重载形式:

private static Remote exportObject(Remote obj, UnicastServerRef sref)
  throws RemoteException
{
  // if obj extends UnicastRemoteObject, set its ref.
  if (obj instanceof UnicastRemoteObject) {
    ((UnicastRemoteObject) obj).ref = sref;
  }
  return sref.exportObject(obj, null, false);
}

然后这里的if语句显然是可以进入的,于是此时会将我们UnicastRemoteObject对象的ref属性赋值为之前的UnicastServerRef对象。

现在的关系是:

UnicastRemoteObject Obj.ref = UnicastServerRef Obj
UnicastServerRef Obj.ref = R

接着调用了UnicastServerRef对象的exportObject方法(没错,这个类也有exportObject方法)

Stub

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;
}

首先这里得到了当前实例的Class对象,然后可以看到这里创建了一个名为stub的Remote对象(非常重要!!)。

跟进其赋值语句Util.createProxy(implClass, getClientRef(), forceStubUse),我们首先来确认下他的参数,首先implClass就是当前实例的Class对象,也就是RMIinterfaceimpl.class,跟进getClientRef()

protected RemoteRef getClientRef() {
  return new UnicastRef(ref);
}

也就是用我们现在的这个UnicastServerRef对象的ref属性作为参数创建了一个UnicastRef对象。而前面大家应该还有印象现在的这个UnicastServerRef对象的ref属性,就是R!看下UnicastRef的构造方法

public UnicastRef(LiveRef liveRef) {
    ref = liveRef;
}

总结下也就是说通过getClientRef(),我们得到了ref属性也为R的UnicastRef对象。

createProxy(implClass, getClientRef(), forceStubUse)的第三个参数是UnicastServerRef对象的一个属性,其默认值为False.

我们跟进去createProxy方法:

public static Remote createProxy(Class<?> implClass,
                                 RemoteRef clientRef,
                                 boolean forceStubUse)
  throws StubNotFoundException
{
  Class<?> remoteClass;

  try {
    remoteClass = getRemoteClass(implClass);
  } catch (ClassNotFoundException ex ) {
    throw new StubNotFoundException(
      "object does not implement a remote interface: " +
      implClass.getName());
  }

  if (forceStubUse ||
      !(ignoreStubClasses || !stubClassExists(remoteClass)))
  {
    return createStub(remoteClass, clientRef);
  }
  final ClassLoader loader = implClass.getClassLoader();
  final Class<?>[] interfaces = getRemoteInterfaces(implClass);
  final InvocationHandler handler = new RemoteObjectInvocationHandler(clientRef);
  try {
    return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
      public Remote run() {
        return (Remote) Proxy.newProxyInstance(loader,
                                               interfaces,
                                               handler);
      }});
  } catch (IllegalArgumentException e) {
    throw new StubNotFoundException("unable to create proxy", e);
  }
}

这里首先调用getRemoteClass(implClass)得到remoteClass,跟进去

private static Class<?> getRemoteClass(Class<?> cl)
  throws ClassNotFoundException
{
  while (cl != null) {
    Class<?>[] interfaces = cl.getInterfaces();
    for (int i = interfaces.length -1; i >= 0; i--) {
      if (Remote.class.isAssignableFrom(interfaces[i]))
        return cl;          // this class implements remote object
    }
    cl = cl.getSuperclass();
  }
  throw new ClassNotFoundException(
    "class does not implement java.rmi.Remote");
}

功能实际上就是检查你传入的这个类对象是否实现了Remote接口或其子接口,看下功能就知道此时返回的还是我们这个类对象没有区别。

接下来进入一个if判断语句:

if (forceStubUse ||
    !(ignoreStubClasses || !stubClassExists(remoteClass)))
{
  return createStub(remoteClass, clientRef);
}

要进入if语句,要么forceStubUse为true,要么(ignoreStubClasses为false且stubClassExists(remoteClass)为true)

forceStubUse是我们之前createProxy时传入的,为false;要使ignoreStubClasses为true是需要在启动jvm时手动设置或者通过Systeml类设置的,否则默认为false,很显然我们没有如此设置 而stubClassExists(remoteClass)方法呢,我们也跟进去:

private static boolean stubClassExists(Class<?> remoteClass) {
  if (!withoutStubs.containsKey(remoteClass)) {
    try {
      Class.forName(remoteClass.getName() + "_Stub",
                    false,
                    remoteClass.getClassLoader());
      return true;

    } catch (ClassNotFoundException cnfe) {
      withoutStubs.put(remoteClass, null);
    }
  }
  return false;
}

其实就是检查是否已经存在了stub类(在我们这个例子中是RMIinterfaceimpl_Stub.class),那显然没有嘛,因此返回false,所以此时其实是不会进入if语句的。

接下来的流程其实就是我们经典的创建动态代理的方法了:

final ClassLoader loader = implClass.getClassLoader();
final Class<?>[] interfaces = getRemoteInterfaces(implClass);
final InvocationHandler handler =
  new RemoteObjectInvocationHandler(clientRef);

/* REMIND: private remote interfaces? */

try {
  return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
    public Remote run() {
      return (Remote) Proxy.newProxyInstance(loader,
                                             interfaces,
                                             handler);
    }});
}

其中interfaces其实就是我们当前对象实现的接口(在这个案例中为RMIinterface)。我们来关注来Invocationhandler(其中ref传参为clientRef)

public RemoteObjectInvocationHandler(RemoteRef ref) {
  super(ref);
  if (ref == null) {
    throw new NullPointerException();
  }
}
protected RemoteObject(RemoteRef newref) {
  ref = newref;
}

所以,其实对RemoteObjectInvocationHandler对象的初始化,也是在为其ref属性赋予了属性UnicastRef!

所以总结一下其实stub是一个动态代理对象,而其ref属性仍然为我们前面创建的UnicastRef.

继续回到exportObject方法,接下来的操作为:

if (stub instanceof RemoteStub) {
  setSkeleton(impl);
}

也就是判断当前的stub是否是Remote及其子类,但是目前还不是,所以先跳过

Target

接下来执行的是:

Target target = new Target(impl, this, stub, ref.getObjID(), permanent)
public Target(Remote impl, Dispatcher disp, Remote stub, ObjID id,
              boolean permanent)
{
  this.weakImpl = new WeakRef(impl, ObjectTable.reapQueue);
  this.disp = disp;
  this.stub = stub;
  this.id = id;
  this.acc = AccessController.getContext();

  /*
         * Fix for 4149366: so that downloaded parameter types unmarshalled
         * for this impl will be compatible with types known only to the
         * impl class's class loader (when it's not identical to the
         * exporting thread's context class loader), mark the impl's class
         * loader as the loader to use as the context class loader in the
         * server's dispatch thread while a call to this impl is being
         * processed (unless this exporting thread's context class loader is
         * a child of the impl's class loader, such as when a registry is
         * exported by an application, in which case this thread's context
         * class loader is preferred).
         */
  ClassLoader threadContextLoader =
    Thread.currentThread().getContextClassLoader();
  ClassLoader serverLoader = impl.getClass().getClassLoader();
  if (checkLoaderAncestry(threadContextLoader, serverLoader)) {
    this.ccl = threadContextLoader;
  } else {
    this.ccl = serverLoader;
  }

  this.permanent = permanent;
  if (permanent) {
    pinImpl();
  }
}

可以看到这里主要是进行了一个封装操作,将我们之前生成的各种对象封装到了一起,回顾下相关的参数:

impl是我们自己对象本身也就是一个RMIinterfaceimpl对象
disp是之前生成的UnicastServerRef对象
stub也是之前生成的stub对象一个动态代理对象
ref.getObjID()UnicastServerRef的ref属性是R其ObjID在前文中创建过
permanent是之前传入的为false

exportObject

接下来执行的是

ref.exportObject(target);

没错这里又涉及到了exportObject,注意,当前我们处于UnicastServerRef对象中,要调用其ref属性的exportObject方法,而其ref属性又是我们之前反复提过的R,看下其exportObject方法:

public void exportObject(Target target) throws RemoteException {
  ep.exportObject(target);
}

这里又嵌套调了其ep属性的exportObject方法并将target作为参数传递进去,而其ep属性则是一个TCPEndpoint对象。

也就是说到目前,其实实现的是

UnicastRemoteObject.exportObject --> UnicastServerRef.exportObject --> R.exportObject --> TCPEndpoint.exportObject

的链式调用

其实这里面跟进去会发现其实TCPEndpoint.exportObject里面调用的还是他transport属性(一个TCPTransport对象)的exportObject方法(人麻了)

public void exportObject(Target target) throws RemoteException {
  /*
         * Ensure that a server socket is listening, and count this
         * export while synchronized to prevent the server socket from
         * being closed due to concurrent unexports.
         */
  synchronized (this) {
    listen();
    exportCount++;
  }

  /*
         * Try to add the Target to the exported object table; keep
         * counting this export (to keep server socket open) only if
         * that succeeds.
         */
  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);
    TCPEndpoint ep = getEndpoint();
    int port = ep.getPort();

    if (server == null) {
        if (tcpLog.isLoggable(Log.BRIEF)) {
            tcpLog.log(Log.BRIEF,
                "(port " + port + ") create server socket");
        }

        try {
            server = ep.newServerSocket();
            /*
             * Don't retry ServerSocket if creation fails since
             * "port in use" will cause export to hang if an
             * RMIFailureHandler is not installed.
             */
            Thread t = AccessController.doPrivileged(
                new NewThreadAction(new AcceptLoop(server),
                                    "TCP Accept-" + port, true));
            t.start();
        } catch (java.net.BindException e) {
            throw new ExportException("Port already in use: " + port, e);
        } catch (IOException e) {
            throw new ExportException("Listen failed on port: " + port, e);
        }

    } else {
        // otherwise verify security access to existing server socket
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkListen(port);
        }
    }
}

这里的主要流程就是调用了ep.newServerSocket()方法:

ServerSocket newServerSocket() throws IOException {
    if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
        TCPTransport.tcpLog.log(Log.VERBOSE,
            "creating server socket on " + this);
    }

    RMIServerSocketFactory serverFactory = ssf;
    if (serverFactory == null) {
        serverFactory = chooseFactory();
    }
    ServerSocket server = serverFactory.createServerSocket(listenPort);

    // if we listened on an anonymous port, set the default port
    // (for this socket factory)
    if (listenPort == 0)
        setDefaultPort(server.getLocalPort(), csf, ssf);

    return server;
}

这里实际上就是开启了一个新线程开启一个socket服务并监听等待用户连接。值得注意的是当listenPort==0时(也就是我们现在的情况),会执行:setDefaultPort(server.getLocalPort(), csf, ssf),最终会随机选择一个可用的端口发布服务

image-20240319111530432

实际上此时Target对象已经被发布出去了,但是我们接着看这个TCPTransport对象的exportObject方法,其在之后又调用了其父类的exportObject方法

image-20240319112141487

看下相关代码:

public void exportObject(Target target) throws RemoteException {
    target.setExportedTransport(this);
    ObjectTable.putTarget(target);
}

setExportedTransport()其实就是在将当前这个TCPTransport对象赋给了Target对象的exportedTransport属性。

void setExportedTransport(Transport exportedTransport) {
    if (this.exportedTransport == null) {
        this.exportedTransport = exportedTransport;
    }
}

至于下面的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) {
        /**
         * Do nothing if impl has already been collected (see 6597112). Check while
         * holding tableLock to ensure that Reaper cannot process weakImpl in between
         * null check and put/increment effects.
         */
        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();
            }
        }
    }
}

首先这里看起来调用了DGCImpl的一些静态方法,在执行静态方法前会执行DGCImpl的静态代码块(也只会在类加载的时候执行一次)。

static {
        /*
         * "Export" the singleton DGCImpl in a context isolated from
         * the arbitrary current thread context.
         */
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ClassLoader savedCcl =
                    Thread.currentThread().getContextClassLoader();
                try {
                    Thread.currentThread().setContextClassLoader(
                        ClassLoader.getSystemClassLoader());

                    /*
                     * Put remote collector object in table by hand to prevent
                     * listen on port.  (UnicastServerRef.exportObject would
                     * cause transport to listen.)
                     */
                    try {
                        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);

                        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中的垃圾处理对象,在静态代码块中其实他的流程和之前创建stub的差不多,都是调用了createProxy方法,不过由于这里其forceStubUse参数为true

image-20240319120123114

所以会通过类加载的方法直接从系统加载DGCImpl_Stub类并初始化:

image-20240319120509477

其他的步骤基本上一样的,都是创建了一个Target对象并执行了ObjectTable.putTarget(target)

回过头来看这个putTarget方法,核心语句其实是:

objTable.put(oe, target);
implTable.put(weakImpl, target);
其中
private static final Map<ObjectEndpoint,Target> objTable = new HashMap<>();
private static final Map<WeakRef,Target> implTable = new HashMap<>();

可以看到相当于往内置的两个Map中添加了键值对,键分别是Target的Endpoint和weakImpl属性而值都是Target.

此时我们终于完成了上面R对象的exportObject方法执行。离UnicastServerRef对象的exportObject方法执行完成也只剩下:

hashToMethod_Map = hashToMethod_Maps.get(implClass)

而这个是个get操作,对结果不成影响,至此我们便完成了完成的创建远程对象的过程(真漫长啊)。

创建注册中心

我们通过Registry registry = LocateRegistry.createRegistry(1099);创建了一个Registry对象

public static Registry createRegistry(int port) throws RemoteException {
  return new RegistryImpl(port);
}

跟进RegistryImpl()

public RegistryImpl(int port)
  throws RemoteException
{
  if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
    // grant permission for default port only.
    try {
      AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
        public Void run() throws RemoteException {
          LiveRef lref = new LiveRef(id, port);
          setup(new UnicastServerRef(lref));
          return null;
        }
      }, null, new SocketPermission("localhost:"+port, "listen,accept"));
    } catch (PrivilegedActionException pae) {
      throw (RemoteException)pae.getException();
    }
  } else {
    LiveRef lref = new LiveRef(id, port);
    setup(new UnicastServerRef(lref));
  }
}

这里if判断进行了端口验证和安全检查,但是安全检查我们过不去,所以执行else代码块

这里和之前类似,创建了一个LiveRef对象(这次我们计为L)并将id和port作为构造方法的参数传入。这个构造方法之前我们也用过。区别就是之前R中的id我们是通过new ObjID()传入了id属性,而现在我们的id属性是RegistryImpl的静态属性:

private static ObjID id = new ObjID(ObjID.REGISTRY_ID);

而UnicastServerRef对象的构造也和之前一样,将LiveRef对象作为参数传入并作为其ref属性。

接下来我们来看setup方法

private void setup(UnicastServerRef uref)
    throws RemoteException
{
    /* Server ref must be created and assigned before remote
     * object 'this' can be exported.
     */
    ref = uref;
    uref.exportObject(this, null, true);
}

首先将UnicastServerRef对象赋予RegistryImpl的ref属性,这里RegistryImpl起到的地位其实就相当于我们前面使用的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;
}

此时第一个参数impl为RegistryImpl对象,第三个参数为true,来看下此时Util.createProxy的区别

if (forceStubUse ||
    !(ignoreStubClasses || !stubClassExists(remoteClass)))
{
  return createStub(remoteClass, clientRef);
}

区别仅仅在于RegistryImpl对象和之前的DGCImpl一样,都是java的内置类且有RegistryImpl_Stub这个类,所以其stub会直接利用反射构造

image-20240319144559363

在下面的if语句判断中

if (stub instanceof RemoteStub) {
  setSkeleton(impl);
}

由于RegistryImpl_Stub继承了RemoteStub,所以会执行setSkeleton(impl)

public void setSkeleton(Remote impl) throws RemoteException {
  if (!withoutSkeletons.containsKey(impl.getClass())) {
    try {
      skel = Util.createSkeleton(impl);
    } catch (SkeletonNotFoundException e) {
      /*
                 * Ignore exception for skeleton class not found, because a
                 * skeleton class is not necessary with the 1.2 stub protocol.
                 * Remember that this impl's class does not have a skeleton
                 * class so we don't waste time searching for it again.
                 */
      withoutSkeletons.put(impl.getClass(), null);
    }
  }
}

首先会执行createSkeleton()方法:

static Skeleton createSkeleton(Remote object)
  throws SkeletonNotFoundException
{
  Class<?> cl;
  try {
    cl = getRemoteClass(object.getClass());
  } catch (ClassNotFoundException ex ) {
    throw new SkeletonNotFoundException(
      "object does not implement a remote interface: " +
      object.getClass().getName());
  }

  // now try to load the skeleton based ont he name of the class
  String skelname = cl.getName() + "_Skel";
  try {
    Class<?> skelcl = Class.forName(skelname, false, cl.getClassLoader());

    return (Skeleton)skelcl.newInstance();
  } catch (ClassNotFoundException ex) {
    throw new SkeletonNotFoundException("Skeleton class not found: " +
                                        skelname, ex);
  } catch (InstantiationException ex) {
    throw new SkeletonNotFoundException("Can't create skeleton: " +
                                        skelname, ex);
  } catch (IllegalAccessException ex) {
    throw new SkeletonNotFoundException("No public constructor: " +
                                        skelname, ex);
  } catch (ClassCastException ex) {
    throw new SkeletonNotFoundException(
      "Skeleton not of correct class: " + skelname, ex);
  }
}

这里其实操作和之前createStub很像,尝试获取名为Registryimpl_Skel的类对象并返回一个实例。又由于

skel = Util.createSkeleton(impl);所以此时我们的UnicastServerRef对象的skel属性为一个Skeleton对象,

在后面的Target构造方法中,这个UnicastServerRef又被传入到disp属性中,而在之前的第一步创建远程对象的那步中,他封装的UnicastServerRef对象的disp属性为null,接下来和之前一样了,创建Target然后发布。最后把Target放到ObjectTable里面。

bind

我们通过registry.bind("test",rmIinterfaceimpl)来将远程对象绑定到registry上

public void bind(String name, Remote obj)
  throws RemoteException, AlreadyBoundException, AccessException
{
  checkAccess("Registry.bind");
  synchronized (bindings) {
    Remote curr = bindings.get(name);
    if (curr != null)
      throw new AlreadyBoundException(name);
    bindings.put(name, obj);
  }
}

这是最简单的一步啦!首先进行了checkAccess()进行了检查,但是这个检查都是可以正常过的。然后就检查你想绑定的关键字是否已经存在了(就是我们用的”test”),假如不存在则向registry对象中的bindings哈希表推入一个键值对,键为绑定的名称(test),值为远程对象。

private Hashtable<String, Remote> bindings = new Hashtable<>(101);

至此我们完成了远程对象和registry的注册,接下来开始就涉及客户端的环节了。

获取registry

接下来client将通过registry获得远程对象。在获得远程对象之前,client将得到一个registry对象。Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099)

public static Registry getRegistry(String host, int port)
  throws RemoteException
{
  return getRegistry(host, port, null);
}

继续跟下getRegistry的重载方法:

public static Registry getRegistry(String host, int port,
                                   RMIClientSocketFactory csf)
  throws RemoteException
{
  Registry registry = null;

  if (port <= 0)
    port = Registry.REGISTRY_PORT;

  if (host == null || host.length() == 0) {
    // If host is blank (as returned by "file:" URL in 1.0.2 used in
    // java.rmi.Naming), try to convert to real local host name so
    // that the RegistryImpl's checkAccess will not fail.
    try {
      host = java.net.InetAddress.getLocalHost().getHostAddress();
    } catch (Exception e) {
      // If that failed, at least try "" (localhost) anyway...
      host = "";
    }
  }

  /*
         * Create a proxy for the registry with the given host, port, and
         * client socket factory.  If the supplied client socket factory is
         * null, then the ref type is a UnicastRef, otherwise the ref type
         * is a UnicastRef2.  If the property
         * java.rmi.server.ignoreStubClasses is true, then the proxy
         * returned is an instance of a dynamic proxy class that implements
         * the Registry interface; otherwise the proxy returned is an
         * instance of the pregenerated stub class for RegistryImpl.
         **/
  LiveRef liveRef =
    new LiveRef(new ObjID(ObjID.REGISTRY_ID),
                new TCPEndpoint(host, port, csf, null),
                false);
  RemoteRef ref =
    (csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);

  return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}

我们可以看到,与我们原先预想的不同,Client并非通过反序列化的方法从Registry获得registry对象,而是直接在本地构造了一个Registry对象。最后返回的是一个RegistryImpl_Stub对象

img

获取远程对象

RMIinterface test = (RMIinterface) registry.lookup("test");

在获取到RegistryImpl_Stub对象后,我们使用lookup进行查询得到相应的远程对象,来看下lookup()的实现:

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
  try {
    RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

    try {
      ObjectOutput var3 = var2.getOutputStream();
      var3.writeObject(var1);
    } catch (IOException var18) {
      throw new MarshalException("error marshalling arguments", var18);
    }

    super.ref.invoke(var2);

    Remote var23;
    try {
      ObjectInput var6 = var2.getInputStream();
      var23 = (Remote)var6.readObject();
    } catch (IOException var15) {
      throw new UnmarshalException("error unmarshalling return", var15);
    } catch (ClassNotFoundException var16) {
      throw new UnmarshalException("error unmarshalling return", var16);
    } finally {
      super.ref.done(var2);
    }

    return var23;
  } catch (RuntimeException var19) {
    throw var19;
  } catch (RemoteException var20) {
    throw var20;
  } catch (NotBoundException var21) {
    throw var21;
  } catch (Exception var22) {
    throw new UnexpectedException("undeclared checked exception", var22);
  }
}

首先利用UnicastRef.newCall方法发起一个请求和registry连接并封装为一个StreamRemoteCall.

public RemoteCall newCall(RemoteObject obj, Operation[] ops, int opnum,
                          long hash)
  throws RemoteException
{
  clientRefLog.log(Log.BRIEF, "get connection");

  Connection conn = ref.getChannel().newConnection();
  try {
    clientRefLog.log(Log.VERBOSE, "create call context");

    /* log information about the outgoing call */
    if (clientCallLog.isLoggable(Log.VERBOSE)) {
      logClientCall(obj, ops[opnum]);
    }

    RemoteCall call =
      new StreamRemoteCall(conn, ref.getObjID(), opnum, hash);
    try {
      marshalCustomCallData(call.getOutputStream());
    } catch (IOException e) {
      throw new MarshalException("error marshaling " +
                                 "custom call data");
    }
    return call;
  } catch (RemoteException e) {
    ref.getChannel().free(conn, false);
    throw e;
  }
}

之后调用RegistryImpl_Stub对象的ref属性,也即一个UnicastRef对象的invoke方法并将刚刚创建的RemoteCall作为参数传入。

public void invoke(RemoteCall call) throws Exception {
    try {
        clientRefLog.log(Log.VERBOSE, "execute call");

        call.executeCall();

    } catch (RemoteException e) {
        /*
         * Call did not complete; connection can't be reused.
         */
        clientRefLog.log(Log.BRIEF, "exception: ", e);
        free(call, false);
        throw e;

    } catch (Error e) {
        /* If errors occurred, the connection is most likely not
         *  reusable.
         */
        clientRefLog.log(Log.BRIEF, "error: ", e);
        free(call, false);
        throw e;

    } catch (RuntimeException e) {
        /*
         * REMIND: Since runtime exceptions are no longer wrapped,
         * we can't assue that the connection was left in
         * a reusable state. Is this okay?
         */
        clientRefLog.log(Log.BRIEF, "exception: ", e);
        free(call, false);
        throw e;

    } catch (Exception e) {
        /*
         * Assume that these other exceptions are user exceptions
         * and leave the connection in a reusable state.
         */
        clientRefLog.log(Log.BRIEF, "exception: ", e);
        free(call, true);
        /* reraise user (and unknown) exceptions. */
        throw e;
    }

    /*
     * Don't free the connection if an exception did not
     * occur because the stub needs to unmarshal the
     * return value. The connection will be freed
     * by a call to the "done" method.
     */
}

可以看到又调用了RemoteCall对象的executeCall()方法:

public void executeCall() throws Exception {
  byte returnType;

  // read result header
  DGCAckHandler ackHandler = null;
  try {
    if (out != null) {
      ackHandler = out.getDGCAckHandler();
    }
    releaseOutputStream();
    DataInputStream rd = new DataInputStream(conn.getInputStream());
    byte op = rd.readByte();
    if (op != TransportConstants.Return) {
      if (Transport.transportLog.isLoggable(Log.BRIEF)) {
        Transport.transportLog.log(Log.BRIEF,
                                   "transport return code invalid: " + op);
      }
      throw new UnmarshalException("Transport return code invalid");
    }
    getInputStream();
    returnType = in.readByte();
    in.readID();        // id for DGC acknowledgement
  } catch (UnmarshalException e) {
    throw e;
  } catch (IOException e) {
    throw new UnmarshalException("Error unmarshaling return header",
                                 e);
  } finally {
    if (ackHandler != null) {
      ackHandler.release();
    }
  }

  // read return value
  switch (returnType) {
    case TransportConstants.NormalReturn:
      break;

    case TransportConstants.ExceptionalReturn:
      Object ex;
      try {
        ex = in.readObject();
      } catch (Exception e) {
        throw new UnmarshalException("Error unmarshaling return", e);
      }

      // An exception should have been received,
      // if so throw it, else flag error
      if (ex instanceof Exception) {
        exceptionReceivedFromServer((Exception) ex);
      } else {
        throw new UnmarshalException("Return type not Exception");
      }
      // Exception is thrown before fallthrough can occur
    default:
      if (Transport.transportLog.isLoggable(Log.BRIEF)) {
        Transport.transportLog.log(Log.BRIEF,
                                   "return code invalid: " + returnType);
      }
      throw new UnmarshalException("Return code invalid");
  }
}

这里实现的是接受registry传回来的字节流,值得注意的是其会判断返回的returnType,当其为异常时,会尝试对返回的字节流反序列化返回对象:

case TransportConstants.ExceptionalReturn:
Object ex;
try {
  ex = in.readObject();
} catch (Exception e) {
  throw new UnmarshalException("Error unmarshaling return", e);
}

很显然这里有一个反序列化漏洞,恶意registry可以可以返回异常并构造恶意对象。而往上来说,也就是说invoke()-->executeCall()这个过程都有可能被构造这种基于异常的反序列化攻击,而在客户端,不止lookup方法,bind,list方法都会调用invoke方法。

回到前面lookup方法的内容

image-20220401163740831

可以看到在这里将远程传输的字节流进行了反序列化并将其转换成一个remote类型对象。很显然这里也有一个反序列化漏洞点,假如注册中心是恶意的,除了上面通过异常进行的反序列化攻击,客户端进行lookup时也能通过返回正常的对象进行反序列化攻击。

注册中心返回远程对象

之前分析过发布远程对象时会调用TCPTransprt#listen(前面提过注册中心也有发布远程对象的过程),实际上listen里面开启了一个AcceptLoop线程,接收到网络请求时会调用它的run方法。

最后分析调用一下会发现处理请求有如下调用链

listen->AcceptLoop.run()->executeAcceptLoop()->ConnectionHandler.run()->run0()->handleMessages()
void handleMessages(Connection conn, boolean persistent) {
  int port = getEndpoint().getPort();

  try {
    DataInputStream in = new DataInputStream(conn.getInputStream());
    do {
      int op = in.read();     // transport op
      if (op == -1) {
        if (tcpLog.isLoggable(Log.BRIEF)) {
          tcpLog.log(Log.BRIEF, "(port " +
                     port + ") connection closed");
        }
        break;
      }

      if (tcpLog.isLoggable(Log.BRIEF)) {
        tcpLog.log(Log.BRIEF, "(port " + port +
                   ") op = " + op);
      }

      switch (op) {
        case TransportConstants.Call:
          // service incoming RMI call
          RemoteCall call = new StreamRemoteCall(conn);
          if (serviceCall(call) == false)
            return;
          break;

        case TransportConstants.Ping:
          // send ack for ping
          DataOutputStream out =
            new DataOutputStream(conn.getOutputStream());
          out.writeByte(TransportConstants.PingAck);
          conn.releaseOutputStream();
          break;

        case TransportConstants.DGCAck:
          DGCAckHandler.received(UID.read(in));
          break;

        default:
          throw new IOException("unknown transport op " + op);
      }
    } while (persistent);

  } catch (IOException e) {
    // exception during processing causes connection to close (below)
    if (tcpLog.isLoggable(Log.BRIEF)) {
      tcpLog.log(Log.BRIEF, "(port " + port +
                 ") exception: ", e);
    }
  } finally {
    try {
      conn.close();
    } catch (IOException ex) {
      // eat exception
    }
  }
}

可以看到基本上是根据不同的op来执行不同的操作。一般来说会执行第一个case:

case TransportConstants.Call:
// service incoming RMI call
RemoteCall call = new StreamRemoteCall(conn);
if (serviceCall(call) == false)
  return;
break;

serviceCall

看下serviceCall方法的实现:

public boolean serviceCall(final RemoteCall call) {
  try {
    /* read object id */
    final Remote impl;
    ObjID id;

    try {
      id = ObjID.read(call.getInputStream());
    } catch (java.io.IOException e) {
      throw new MarshalException("unable to read objID", e);
    }

    /* get the remote object */
    Transport transport = id.equals(dgcID) ? null : this;
    Target target =
      ObjectTable.getTarget(new ObjectEndpoint(id, transport));

    if (target == null || (impl = target.getImpl()) == null) {
      throw new NoSuchObjectException("no such object in table");
    }

    final Dispatcher disp = target.getDispatcher();
    target.incrementCallCount();
    try {
      /* call the dispatcher */
      transportLog.log(Log.VERBOSE, "call dispatcher");

      final AccessControlContext acc =
        target.getAccessControlContext();
      ClassLoader ccl = target.getContextClassLoader();

      ClassLoader savedCcl = Thread.currentThread().getContextClassLoader();

      try {
        setContextClassLoader(ccl);
        currentTransport.set(this);
        try {
          java.security.AccessController.doPrivileged(
            new java.security.PrivilegedExceptionAction<Void>() {
              public Void run() throws IOException {
                checkAcceptPermission(acc);
                disp.dispatch(impl, call);
                return null;
              }
            }, acc);
        } catch (java.security.PrivilegedActionException pae) {
          throw (IOException) pae.getException();
        }
      } finally {
        setContextClassLoader(savedCcl);
        currentTransport.set(null);
      }

    } catch (IOException ex) {
      transportLog.log(Log.BRIEF,
                       "exception thrown by dispatcher: ", ex);
      return false;
    } finally {
      target.decrementCallCount();
    }

  } catch (RemoteException e) {

    // if calls are being logged, write out exception
    if (UnicastServerRef.callLog.isLoggable(Log.BRIEF)) {
      // include client host name if possible
      String clientHost = "";
      try {
        clientHost = "[" +
          RemoteServer.getClientHost() + "] ";
      } catch (ServerNotActiveException ex) {
      }
      String message = clientHost + "exception: ";
      UnicastServerRef.callLog.log(Log.BRIEF, message, e);
    }

    /* We will get a RemoteException if either a) the objID is
             * not readable, b) the target is not in the object table, or
             * c) the object is in the midst of being unexported (note:
             * NoSuchObjectException is thrown by the incrementCallCount
             * method if the object is being unexported).  Here it is
             * relatively safe to marshal an exception to the client
             * since the client will not have seen a return value yet.
             */
    try {
      ObjectOutput out = call.getResultStream(false);
      UnicastServerRef.clearStackTraces(e);
      out.writeObject(e);
      call.releaseOutputStream();

    } catch (IOException ie) {
      transportLog.log(Log.BRIEF,
                       "exception thrown marshalling exception: ", ie);
      return false;
    }
  }

  return true;
}

首先根据客户端传来的Endpoint的ID生成一个新的endpoint,然后在ObjectTable.getTarget(new ObjectEndpoint(id, transport))中获取了现在已有的具有相同endpoint的Target(ID又是根据ip和port生成的,所以可以保证本地和注册中心的ID一致)

Transport transport = id.equals(dgcID) ? null : this;
Target target = ObjectTable.getTarget(new ObjectEndpoint(id, transport));

之后关键的点在于当我们获取了disp属性(也就是一个UnicastServerRef对象)后,会执行其dispatch方法:

image-20220401203648415

public void dispatch(Remote obj, RemoteCall call) throws IOException {
  // positive operation number in 1.1 stubs;
  // negative version number in 1.2 stubs and beyond...
  int num;
  long op;

  try {
    // read remote call header
    ObjectInput in;
    try {
      in = call.getInputStream();
      num = in.readInt();
      if (num >= 0) {
        if (skel != null) {
          oldDispatch(obj, call, num);
          return;
        } else {
          throw new UnmarshalException(
            "skeleton class not found but required " +
            "for client version");
        }
      }
      op = in.readLong();
    } catch (Exception readEx) {
      throw new UnmarshalException("error unmarshalling call header",
                                   readEx);
    }

    /*
             * Since only system classes (with null class loaders) will be on
             * the execution stack during parameter unmarshalling for the 1.2
             * stub protocol, tell the MarshalInputStream not to bother trying
             * to resolve classes using its superclasses's default method of
             * consulting the first non-null class loader on the stack.
             */
    MarshalInputStream marshalStream = (MarshalInputStream) in;
    marshalStream.skipDefaultResolveClass();

    Method method = hashToMethod_Map.get(op);
    if (method == null) {
      throw new UnmarshalException("unrecognized method hash: " +
                                   "method not supported by remote object");
    }

    // if calls are being logged, write out object id and operation
    logCall(obj, method);

    // unmarshal parameters
    Class<?>[] types = method.getParameterTypes();
    Object[] params = new Object[types.length];

    try {
      unmarshalCustomCallData(in);
      for (int i = 0; i < types.length; i++) {
        params[i] = unmarshalValue(types[i], in);
      }
    } catch (java.io.IOException e) {
      throw new UnmarshalException(
        "error unmarshalling arguments", e);
    } catch (ClassNotFoundException e) {
      throw new UnmarshalException(
        "error unmarshalling arguments", e);
    } finally {
      call.releaseInputStream();
    }

    // make upcall on remote object
    Object result;
    try {
      result = method.invoke(obj, params);
    } catch (InvocationTargetException e) {
      throw e.getTargetException();
    }

    // marshal return value
    try {
      ObjectOutput out = call.getResultStream(true);
      Class<?> rtype = method.getReturnType();
      if (rtype != void.class) {
        marshalValue(rtype, result, out);
      }
    } catch (IOException ex) {
      throw new MarshalException("error marshalling return", ex);
      /*
                 * This throw is problematic because when it is caught below,
                 * we attempt to marshal it back to the client, but at this
                 * point, a "normal return" has already been indicated,
                 * so marshalling an exception will corrupt the stream.
                 * This was the case with skeletons as well; there is no
                 * immediately obvious solution without a protocol change.
                 */
    }
  } catch (Throwable e) {
    logCallException(e);

    ObjectOutput out = call.getResultStream(false);
    if (e instanceof Error) {
      e = new ServerError(
        "Error occurred in server thread", (Error) e);
    } else if (e instanceof RemoteException) {
      e = new ServerException(
        "RemoteException occurred in server thread",
        (Exception) e);
    }
    if (suppressStackTraces) {
      clearStackTraces(e);
    }
    out.writeObject(e);
  } finally {
    call.releaseInputStream(); // in case skeleton doesn't
    call.releaseOutputStream();
  }
}

当skel不为null时会调用oldDispatch()

public void oldDispatch(Remote obj, RemoteCall call, int op)
  throws IOException
{
  long hash;              // hash for matching stub with skeleton

  try {
    // read remote call header
    ObjectInput in;
    try {
      in = call.getInputStream();
      try {
        Class<?> clazz = Class.forName("sun.rmi.transport.DGCImpl_Skel");
        if (clazz.isAssignableFrom(skel.getClass())) {
          ((MarshalInputStream)in).useCodebaseOnly();
        }
      } catch (ClassNotFoundException ignore) { }
      hash = in.readLong();
    } catch (Exception readEx) {
      throw new UnmarshalException("error unmarshalling call header",
                                   readEx);
    }

    // if calls are being logged, write out object id and operation
    logCall(obj, skel.getOperations()[op]);
    unmarshalCustomCallData(in);
    // dispatch to skeleton for remote object
    skel.dispatch(obj, call, op, hash);

  } catch (Throwable e) {
    logCallException(e);

    ObjectOutput out = call.getResultStream(false);
    if (e instanceof Error) {
      e = new ServerError(
        "Error occurred in server thread", (Error) e);
    } else if (e instanceof RemoteException) {
      e = new ServerException(
        "RemoteException occurred in server thread",
        (Exception) e);
    }
    if (suppressStackTraces) {
      clearStackTraces(e);
    }
    out.writeObject(e);
  } finally {
    call.releaseInputStream(); // in case skeleton doesn't
    call.releaseOutputStream();
  }
}

在最后调用了skel.dispatch(obj, call, op, hash),这里基本上就是根据不同的op执行不同的操作了

public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
  if (var4 != 4905912898345647071L) {
    throw new SkeletonMismatchException("interface hash mismatch");
  } else {
    RegistryImpl var6 = (RegistryImpl)var1;
    String var7;
    Remote var8;
    ObjectInput var10;
    ObjectInput var11;
    switch (var3) {
      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 1:
        var2.releaseInputStream();
        String[] var97 = var6.list();

        try {
          ObjectOutput var98 = var2.getResultStream(true);
          var98.writeObject(var97);
          break;
        } catch (IOException var92) {
          throw new MarshalException("error marshalling return", var92);
        }
      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);

        try {
          ObjectOutput var9 = var2.getResultStream(true);
          var9.writeObject(var8);
          break;
        } catch (IOException var88) {
          throw new MarshalException("error marshalling return", var88);
        }
      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);
        }
      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);

        try {
          var2.getResultStream(true);
          break;
        } catch (IOException var80) {
          throw new MarshalException("error marshalling return", var80);
        }
      default:
        throw new UnmarshalException("invalid method number");
    }

  }
}

其中:

switch(opnum){
    case 0:// bind(String, Remote)
    case 1: // list()
    case 2: // lookup(String)
    case 3: // rebind(String, Remote)
    case 4: // unbind(String)
}

值得注意的是,我这里用的是jdk8u65进行调试所以对每种方法操作都能进入相应的处理语句进行处理,当版本大于等于jdk8u141时,处理语句变成了:

public void dispatch(java.rmi.Remote obj, java.rmi.server.RemoteCall call, int opnum, long hash)
        throws java.lang.Exception {
    if (hash != interfaceHash)
        throw new java.rmi.server.SkeletonMismatchException("interface hash mismatch");

    sun.rmi.registry.RegistryImpl server = (sun.rmi.registry.RegistryImpl) obj;
    switch (opnum) {
        case 0: // bind(String, Remote)
        {
            // Check access before reading the arguments
            RegistryImpl.checkAccess("Registry.bind");

            java.lang.String $param_String_1;
            java.rmi.Remote $param_Remote_2;
            try {
                java.io.ObjectInput in = call.getInputStream();
                $param_String_1 = (java.lang.String) in.readObject();
                $param_Remote_2 = (java.rmi.Remote) in.readObject();
            } catch (java.io.IOException | java.lang.ClassNotFoundException e) {
                throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
            } finally {
                call.releaseInputStream();
            }
            server.bind($param_String_1, $param_Remote_2);
            try {
                call.getResultStream(true);
            } catch (java.io.IOException e) {
                throw new java.rmi.MarshalException("error marshalling return", e);
            }
            break;
        }

        case 1: // list()
        {
            call.releaseInputStream();
            java.lang.String[] $result = server.list();
            try {
                java.io.ObjectOutput out = call.getResultStream(true);
                out.writeObject($result);
            } catch (java.io.IOException e) {
                throw new java.rmi.MarshalException("error marshalling return", e);
            }
            break;
        }

        case 2: // lookup(String)
        {
            java.lang.String $param_String_1;
            try {
                java.io.ObjectInput in = call.getInputStream();
                $param_String_1 = (java.lang.String) in.readObject();
            } catch (java.io.IOException | java.lang.ClassNotFoundException e) {
                throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
            } finally {
                call.releaseInputStream();
            }
            java.rmi.Remote $result = server.lookup($param_String_1);
            try {
                java.io.ObjectOutput out = call.getResultStream(true);
                out.writeObject($result);
            } catch (java.io.IOException e) {
                throw new java.rmi.MarshalException("error marshalling return", e);
            }
            break;
        }

        case 3: // rebind(String, Remote)
        {
            // Check access before reading the arguments
            RegistryImpl.checkAccess("Registry.rebind");

            java.lang.String $param_String_1;
            java.rmi.Remote $param_Remote_2;
            try {
                java.io.ObjectInput in = call.getInputStream();
                $param_String_1 = (java.lang.String) in.readObject();
                $param_Remote_2 = (java.rmi.Remote) in.readObject();
            } catch (java.io.IOException | java.lang.ClassNotFoundException e) {
                throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
            } finally {
                call.releaseInputStream();
            }
            server.rebind($param_String_1, $param_Remote_2);
            try {
                call.getResultStream(true);
            } catch (java.io.IOException e) {
                throw new java.rmi.MarshalException("error marshalling return", e);
            }
            break;
        }

        case 4: // unbind(String)
        {
            // Check access before reading the arguments
            RegistryImpl.checkAccess("Registry.unbind");

            java.lang.String $param_String_1;
            try {
                java.io.ObjectInput in = call.getInputStream();
                $param_String_1 = (java.lang.String) in.readObject();
            } catch (java.io.IOException | java.lang.ClassNotFoundException e) {
                throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
            } finally {
                call.releaseInputStream();
            }
            server.unbind($param_String_1);
            try {
                call.getResultStream(true);
            } catch (java.io.IOException e) {
                throw new java.rmi.MarshalException("error marshalling return", e);
            }
            break;
        }

在bind,rebind,unbind方法的调用中执行了RegistryImpl.checkAccess("Registry.unbind"),这几个方法只允许本地调用。

此时我们使用了lookup,来看具体代码:

case 2: // lookup(String)
{
  java.lang.String $param_String_1;
  try {
    java.io.ObjectInput in = call.getInputStream();
    $param_String_1 = (java.lang.String) in.readObject();
  } catch (java.io.IOException | java.lang.ClassNotFoundException e) {
    throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
  } finally {
    call.releaseInputStream();
  }
  java.rmi.Remote $result = server.lookup($param_String_1);
  try {
    java.io.ObjectOutput out = call.getResultStream(true);
    out.writeObject($result);
  } catch (java.io.IOException e) {
    throw new java.rmi.MarshalException("error marshalling return", e);
  }
  break;
}

看到使用readObject来从字节输出流中直接反序列化对象,所以这里也存在一个反序列化漏洞,实际上可以看到:不光lookup,bind、rebind、unbind都有类似的漏洞(当然结合jdk8u141的限制,在高版本中就只能调用当中的lookup了),这里其实就是根据传入的名字(“test”)从RegistryImpl对象中获得其绑定的对象,来看后面调用的var6.lookup(var7)

public Remote lookup(String name)
  throws RemoteException, NotBoundException
{
  synchronized (bindings) {
    Remote obj = bindings.get(name);
    if (obj == null)
      throw new NotBoundException(name);
    return obj;
  }
}

其实一目了然了,就是获得在bind环节中bind的远程对象。后续再进行序列化写入字节流传输至客户端。

注意这里的out是ConnectionOutputStream,父类是MarshalOutputStream,MarshalOutputStream的构造方法里调用了enableReplaceObject(true),这样在序列化时会调用它的replaceObject

/**
 * Checks for objects that are instances of java.rmi.Remote
 * that need to be serialized as proxy objects.
 */
protected final Object replaceObject(Object obj) throws IOException {
    if ((obj instanceof Remote) && !(obj instanceof RemoteStub)) {
        Target target = ObjectTable.getTarget((Remote) obj);
        if (target != null) {
            return target.getStub();
        }
    }
    return obj;
}

所以注册中心返回给客户端的远程对象实际上是stub,也就是动态代理对象。同时我们也看的,客户端获取这个stub的过程也是通过skeleton实现的。

客户端调用服务端

得到stub后,下一步流程是客户端通过调用stub的方法String result = test.test();之前分析已经知道remoteObj是一个动态代理对象,那么调用它的方法时自然是走到了调用处理器类里面,跟进RemoteObjectInvocationHandler#invoke

public Object invoke(Object proxy, Method method, Object[] args)
  throws Throwable
{
  if (! Proxy.isProxyClass(proxy.getClass())) {
    throw new IllegalArgumentException("not a proxy");
  }

  if (Proxy.getInvocationHandler(proxy) != this) {
    throw new IllegalArgumentException("handler mismatch");
  }

  if (method.getDeclaringClass() == Object.class) {
    return invokeObjectMethod(proxy, method, args);
  } else if ("finalize".equals(method.getName()) && method.getParameterCount() == 0 &&
             !allowFinalizeInvocation) {
    return null; // ignore
  } else {
    return invokeRemoteMethod(proxy, method, args);
  }
}

调用的是else里面的invokeRemoteMethod

private Object invokeRemoteMethod(Object proxy,
                                  Method method,
                                  Object[] args)
    throws Exception
{
    try {
        if (!(proxy instanceof Remote)) {
            throw new IllegalArgumentException(
                "proxy not Remote instance");
        }
        return ref.invoke((Remote) proxy, method, args,
                          getMethodHash(method));
    } catch (Exception e) {
        if (!(e instanceof RuntimeException)) {
            Class<?> cl = proxy.getClass();
            try {
                method = cl.getMethod(method.getName(),
                                      method.getParameterTypes());
            } catch (NoSuchMethodException nsme) {
                throw (IllegalArgumentException)
                    new IllegalArgumentException().initCause(nsme);
            }
            Class<?> thrownType = e.getClass();
            for (Class<?> declaredType : method.getExceptionTypes()) {
                if (declaredType.isAssignableFrom(thrownType)) {
                    throw e;
                }
            }
            e = new UnexpectedException("unexpected exception", e);
        }
        throw e;
    }
}

最后调用的是UnicastRef里面另一个重载的invoke,一样调用了executeCall,那么这里客户端也可能被攻击(利用之前提到的异常类型的攻击)。

此外可以注意到如果返回值type不是void会调用unmarshalValue

protected static Object unmarshalValue(Class<?> type, ObjectInput in)
    throws IOException, ClassNotFoundException
{
    if (type.isPrimitive()) {
        if (type == int.class) {
            return Integer.valueOf(in.readInt());
        } else if (type == boolean.class) {
            return Boolean.valueOf(in.readBoolean());
        } else if (type == byte.class) {
            return Byte.valueOf(in.readByte());
        } else if (type == char.class) {
            return Character.valueOf(in.readChar());
        } else if (type == short.class) {
            return Short.valueOf(in.readShort());
        } else if (type == long.class) {
            return Long.valueOf(in.readLong());
        } else if (type == float.class) {
            return Float.valueOf(in.readFloat());
        } else if (type == double.class) {
            return Double.valueOf(in.readDouble());
        } else {
            throw new Error("Unrecognized primitive type: " + type);
        }
    } else {
        return in.readObject();
    }
}

当返回值类型不是这几种基础类型时就会调用反序列化,也就是服务端可以通过返回恶意对象来攻击客户端。

服务端响应客户端

至于服务端的相应流程其实和查找远程对象很类似,首先都是到了UnicastServerRef类执行了dispatch方法:

public void dispatch(Remote obj, RemoteCall call) throws IOException {
  // positive operation number in 1.1 stubs;
  // negative version number in 1.2 stubs and beyond...
  int num;
  long op;

  try {
    // read remote call header
    ObjectInput in;
    try {
      in = call.getInputStream();
      num = in.readInt();
      if (num >= 0) {
        if (skel != null) {
          oldDispatch(obj, call, num);
          return;
        } else {
          throw new UnmarshalException(
            "skeleton class not found but required " +
            "for client version");
        }
      }
      op = in.readLong();
    } catch (Exception readEx) {
      throw new UnmarshalException("error unmarshalling call header",
                                   readEx);
    }

    /*
             * Since only system classes (with null class loaders) will be on
             * the execution stack during parameter unmarshalling for the 1.2
             * stub protocol, tell the MarshalInputStream not to bother trying
             * to resolve classes using its superclasses's default method of
             * consulting the first non-null class loader on the stack.
             */
    MarshalInputStream marshalStream = (MarshalInputStream) in;
    marshalStream.skipDefaultResolveClass();

    Method method = hashToMethod_Map.get(op);
    if (method == null) {
      throw new UnmarshalException("unrecognized method hash: " +
                                   "method not supported by remote object");
    }

    // if calls are being logged, write out object id and operation
    logCall(obj, method);

    // unmarshal parameters
    Class<?>[] types = method.getParameterTypes();
    Object[] params = new Object[types.length];

    try {
      unmarshalCustomCallData(in);
      for (int i = 0; i < types.length; i++) {
        params[i] = unmarshalValue(types[i], in);
      }
    } catch (java.io.IOException e) {
      throw new UnmarshalException(
        "error unmarshalling arguments", e);
    } catch (ClassNotFoundException e) {
      throw new UnmarshalException(
        "error unmarshalling arguments", e);
    } finally {
      call.releaseInputStream();
    }

    // make upcall on remote object
    Object result;
    try {
      result = method.invoke(obj, params);
    } catch (InvocationTargetException e) {
      throw e.getTargetException();
    }

    // marshal return value
    try {
      ObjectOutput out = call.getResultStream(true);
      Class<?> rtype = method.getReturnType();
      if (rtype != void.class) {
        marshalValue(rtype, result, out);
      }
    } catch (IOException ex) {
      throw new MarshalException("error marshalling return", ex);
      /*
                 * This throw is problematic because when it is caught below,
                 * we attempt to marshal it back to the client, but at this
                 * point, a "normal return" has already been indicated,
                 * so marshalling an exception will corrupt the stream.
                 * This was the case with skeletons as well; there is no
                 * immediately obvious solution without a protocol change.
                 */
    }
  } catch (Throwable e) {
    logCallException(e);

    ObjectOutput out = call.getResultStream(false);
    if (e instanceof Error) {
      e = new ServerError(
        "Error occurred in server thread", (Error) e);
    } else if (e instanceof RemoteException) {
      e = new ServerException(
        "RemoteException occurred in server thread",
        (Exception) e);
    }
    if (suppressStackTraces) {
      clearStackTraces(e);
    }
    out.writeObject(e);
  } finally {
    call.releaseInputStream(); // in case skeleton doesn't
    call.releaseOutputStream();
  }
}

不过由于远程对象的是没有skel的(只有registry对象有),所以不会执行olddispatch()而是往下走:

Class<?>[] types = method.getParameterTypes();
Object[] params = new Object[types.length];

try {
  unmarshalCustomCallData(in);
  for (int i = 0; i < types.length; i++) {
    params[i] = unmarshalValue(types[i], in);
  }

这里其实就是去获得我们要执行的方法,然后去读取参数并存入params数组,而这里的unmarshalValue()前面提过当type不是基本数据类型时,

protected static Object unmarshalValue(Class<?> type, ObjectInput in)
    throws IOException, ClassNotFoundException
{
    if (type.isPrimitive()) {
        if (type == int.class) {
            return Integer.valueOf(in.readInt());
        } else if (type == boolean.class) {
            return Boolean.valueOf(in.readBoolean());
        } else if (type == byte.class) {
            return Byte.valueOf(in.readByte());
        } else if (type == char.class) {
            return Character.valueOf(in.readChar());
        } else if (type == short.class) {
            return Short.valueOf(in.readShort());
        } else if (type == long.class) {
            return Long.valueOf(in.readLong());
        } else if (type == float.class) {
            return Float.valueOf(in.readFloat());
        } else if (type == double.class) {
            return Double.valueOf(in.readDouble());
        } else {
            throw new Error("Unrecognized primitive type: " + type);
        }
    } else {
        return in.readObject();
    }
}

当type不为基本数据类型时就会对输入的字节流进行反序列化。所以这里服务端也可以被客户端所攻击。

DGC

其实要是一路下来跟这条链的师傅可能会发现,其实中间多次出现了DGC这个类的干扰,比方说在lookup过程中,注册中心会执行了两次去搜索Target的过程,第一次是得到的是stub为RegistyImpl_Stub的Target对象,第二次得到的则是stub为的Target对象(没错,在开始注册远程对象时注册进来的)服务端DGC对象的初始化之前我们已经探究过,现在研究下客户端和注册中心中DGC的应用。

我们来探究下这个过程。

客户端创建DGC

实际上,客户端在本地也创建了一个DGC对象,在执行lookup方法时:

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
  try {
    RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

    try {
      ObjectOutput var3 = var2.getOutputStream();
      var3.writeObject(var1);
    } catch (IOException var18) {
      throw new MarshalException("error marshalling arguments", var18);
    }

    super.ref.invoke(var2);

    Remote var23;
    try {
      ObjectInput var6 = var2.getInputStream();
      var23 = (Remote)var6.readObject();
    } catch (IOException var15) {
      throw new UnmarshalException("error unmarshalling return", var15);
    } catch (ClassNotFoundException var16) {
      throw new UnmarshalException("error unmarshalling return", var16);
    } finally {
      super.ref.done(var2);
    }

    return var23;
  } catch (RuntimeException var19) {
    throw var19;
  } catch (RemoteException var20) {
    throw var20;
  } catch (NotBoundException var21) {
    throw var21;
  } catch (Exception var22) {
    throw new UnexpectedException("undeclared checked exception", var22);
  }
}

当中的super.ref.done(var2):

public void done(RemoteCall call) throws RemoteException {

    /* Done only uses the connection inside the call to obtain the
     * channel the connection uses.  Once all information is read
     * from the connection, the connection may be freed.
     */
    clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");

    /* Free the call connection early. */
    free(call, true);

    try {
        call.done();
    } catch (IOException e) {
        /* WARNING: If the conn has been reused early, then it is
         * too late to recover from thrown IOExceptions caught
         * here. This code is relying on StreamRemoteCall.done()
         * not actually throwing IOExceptions.
         */
    }
}

跟进call.done()

public void done() throws IOException {
    /* WARNING: Currently, the UnicastRef.java invoke methods rely
     * upon this method not throwing an IOException.
     */

    releaseInputStream();
}

继续跟

public void releaseInputStream() throws IOException {
  /* WARNING: Currently, the UnicastRef.java invoke methods rely
         * upon this method not throwing an IOException.
         */

  try {
    if (in != null) {
      // execute MarshalInputStream "done" callbacks
      try {
        in.done();
      } catch (RuntimeException e) {
      }

      // add saved references to DGC table
      in.registerRefs();

      /* WARNING: The connection being passed to done may have
                 * already been freed.
                 */
      in.done(conn);
    }
    conn.releaseInputStream();
  } finally {
    in = null;
  }
}

调用了registerRefs()

void registerRefs() throws IOException {
  if (!incomingRefTable.isEmpty()) {
    for (Map.Entry<Endpoint, List<LiveRef>> entry :
         incomingRefTable.entrySet()) {
      DGCClient.registerRefs(entry.getKey(), entry.getValue());
    }
  }
}

调用了registerRefs()方法:

static void registerRefs(Endpoint ep, List<LiveRef> refs) {
  /*
         * Look up the given endpoint and register the refs with it.
         * The retrieved entry may get removed from the global endpoint
         * table before EndpointEntry.registerRefs() is able to acquire
         * its lock; in this event, it returns false, and we loop and
         * try again.
         */
  EndpointEntry epEntry;
  do {
    epEntry = EndpointEntry.lookup(ep);
  } while (!epEntry.registerRefs(refs));
}

又调用了EndpointEntry的lookup方法:

public static EndpointEntry lookup(Endpoint ep) {
  synchronized (endpointTable) {
    EndpointEntry entry = endpointTable.get(ep);
    if (entry == null) {
      entry = new EndpointEntry(ep);
      endpointTable.put(ep, entry);
      /*
                     * While we are tracking live remote references registered
                     * in this VM, request a maximum latency for inspecting the
                     * entire heap from the local garbage collector, to place
                     * an upper bound on the time to discover remote references
                     * that have become unreachable (see bugid 4171278).
                     */
      if (gcLatencyRequest == null) {
        gcLatencyRequest = GC.requestLatency(gcInterval);
      }
    }
    return entry;
  }
}

又新建了EndpointEntry对象

private EndpointEntry(final Endpoint endpoint) {
  this.endpoint = endpoint;
  try {
    LiveRef dgcRef = new LiveRef(dgcID, endpoint, false);
    dgc = (DGC) Util.createProxy(DGCImpl.class,
                                 new UnicastRef(dgcRef), true);
  } catch (RemoteException e) {
    throw new Error("internal error creating DGC stub");
  }
  renewCleanThread =  AccessController.doPrivileged(
    new NewThreadAction(new RenewCleanThread(),
                        "RenewClean-" + endpoint, true));
  renewCleanThread.start();
}

这里就创建了一个DGCImpl_Stub对象并赋给了dgc属性,接下来调用了:

renewCleanThread =  AccessController.doPrivileged(new NewThreadAction(new RenewCleanThread(),
"RenewClean-" + endpoint, true));

跟进RenewCleanThread()

private class RenewCleanThread implements Runnable {

  public void run() {
    do {
      long timeToWait;
      RefEntry.PhantomLiveRef phantom = null;
      boolean needRenewal = false;
      Set<RefEntry> refsToDirty = null;
      long sequenceNum = Long.MIN_VALUE;

      synchronized (EndpointEntry.this) {
        /*
                         * Calculate time to block (waiting for phantom
                         * reference notifications).  It is the time until the
                         * lease renewal should be done, bounded on the low
                         * end by 1 ms so that the reference queue will always
                         * get processed, and if there are pending clean
                         * requests (remaining because some clean calls
                         * failed), bounded on the high end by the maximum
                         * clean call retry interval.
                         */
        long timeUntilRenew =
          renewTime - System.currentTimeMillis();
        timeToWait = Math.max(timeUntilRenew, 1);
        if (!pendingCleans.isEmpty()) {
          timeToWait = Math.min(timeToWait, cleanInterval);
        }

        /*
                         * Set flag indicating that it is OK to interrupt this
                         * thread now, such as if a earlier lease renewal time
                         * is set, because we are only going to be blocking
                         * and can deal with interrupts.
                         */
        interruptible = true;
      }

      try {
        /*
                         * Wait for the duration calculated above for any of
                         * our phantom references to be enqueued.
                         */
        phantom = (RefEntry.PhantomLiveRef)
          refQueue.remove(timeToWait);
      } catch (InterruptedException e) {
      }

      synchronized (EndpointEntry.this) {
        /*
                         * Set flag indicating that it is NOT OK to interrupt
                         * this thread now, because we may be undertaking I/O
                         * operations that should not be interrupted (and we
                         * will not be blocking arbitrarily).
                         */
        interruptible = false;
        Thread.interrupted();   // clear interrupted state

        /*
                         * If there was a phantom reference enqueued, process
                         * it and all the rest on the queue, generating
                         * clean requests as necessary.
                         */
        if (phantom != null) {
          processPhantomRefs(phantom);
        }

        /*
                         * Check if it is time to renew this entry's lease.
                         */
        long currentTime = System.currentTimeMillis();
        if (currentTime > renewTime) {
          needRenewal = true;
          if (!invalidRefs.isEmpty()) {
            refsToDirty = invalidRefs;
            invalidRefs = new HashSet<>(5);
          }
          sequenceNum = getNextSequenceNum();
        }
      }

      boolean needRenewal_ = needRenewal;
      Set<RefEntry> refsToDirty_ = refsToDirty;
      long sequenceNum_ = sequenceNum;
      AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
          if (needRenewal_) {
            makeDirtyCall(refsToDirty_, sequenceNum_);
          }

          if (!pendingCleans.isEmpty()) {
            makeCleanCalls();
          }
          return null;
        }}, SOCKET_ACC);
    } while (!removed || !pendingCleans.isEmpty());
  }
}

run()方法中调用了makeDirtyCall

private void makeDirtyCall(Set<RefEntry> refEntries, long sequenceNum) {
    assert !Thread.holdsLock(this);

    ObjID[] ids;
    if (refEntries != null) {
        ids = createObjIDArray(refEntries);
    } else {
        ids = emptyObjIDArray;
    }

    long startTime = System.currentTimeMillis();
    try {
        Lease lease =
            dgc.dirty(ids, sequenceNum, new Lease(vmid, leaseValue));
        long duration = lease.getValue();

        long newRenewTime = computeRenewTime(startTime, duration);
        long newExpirationTime = startTime + duration;

        synchronized (this) {
            dirtyFailures = 0;
            setRenewTime(newRenewTime);
            expirationTime = newExpirationTime;
        }

    } catch (Exception e) {
        ......

调用了dgc.dirty()

public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
  try {
    RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);

    try {
      ObjectOutput var6 = var5.getOutputStream();
      var6.writeObject(var1);
      var6.writeLong(var2);
      var6.writeObject(var4);
    } catch (IOException var20) {
      throw new MarshalException("error marshalling arguments", var20);
    }

    super.ref.invoke(var5);

    Lease var24;
    try {
      ObjectInput var9 = var5.getInputStream();
      var24 = (Lease)var9.readObject();
    } catch (IOException var17) {
      throw new UnmarshalException("error unmarshalling return", var17);
    } catch (ClassNotFoundException var18) {
      throw new UnmarshalException("error unmarshalling return", var18);
    } finally {
      super.ref.done(var5);
    }

    return var24;
  } catch (RuntimeException var21) {
    throw var21;
  } catch (RemoteException var22) {
    throw var22;
  } catch (Exception var23) {
    throw new UnexpectedException("undeclared checked exception", var23);
  }
}

调用了从输出流中读入数据并进行了反序列化,也就是说DGC客户端调用dirty时有可能被DGC服务端攻击。而前面的每个stub的调用都会触发ref.done,也就是每次stub的调用都可能被DGC服务端攻击。

客户端解析DGC请求

还是到UnicastServerRef#dispatch,然后DGCImpl_Stub也是jdk内置类,所以进入oldDispatch,最后进入DGCImpl_Skel#dispatch。

public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
  if (var4 != -669196253586618813L) {
    throw new SkeletonMismatchException("interface hash mismatch");
  } else {
    DGCImpl var6 = (DGCImpl)var1;
    ObjID[] var7;
    long var8;
    switch (var3) {
      case 0:
        VMID var39;
        boolean var40;
        try {
          ObjectInput var14 = var2.getInputStream();
          var7 = (ObjID[])var14.readObject();
          var8 = var14.readLong();
          var39 = (VMID)var14.readObject();
          var40 = var14.readBoolean();
        } catch (IOException var36) {
          throw new UnmarshalException("error unmarshalling arguments", var36);
        } catch (ClassNotFoundException var37) {
          throw new UnmarshalException("error unmarshalling arguments", var37);
        } finally {
          var2.releaseInputStream();
        }

        var6.clean(var7, var8, var39, var40);

        try {
          var2.getResultStream(true);
          break;
        } catch (IOException var35) {
          throw new MarshalException("error marshalling return", var35);
        }
      case 1:
        Lease var10;
        try {
          ObjectInput var13 = var2.getInputStream();
          var7 = (ObjID[])var13.readObject();
          var8 = var13.readLong();
          var10 = (Lease)var13.readObject();
        } catch (IOException var32) {
          throw new UnmarshalException("error unmarshalling arguments", var32);
        } catch (ClassNotFoundException var33) {
          throw new UnmarshalException("error unmarshalling arguments", var33);
        } finally {
          var2.releaseInputStream();
        }

        Lease var11 = var6.dirty(var7, var8, var10);

        try {
          ObjectOutput var12 = var2.getResultStream(true);
          var12.writeObject(var11);
          break;
        } catch (IOException var31) {
          throw new MarshalException("error marshalling return", var31);
        }
      default:
        throw new UnmarshalException("invalid method number");
    }

  }
}

还是读入了传递来的数据触发了反序列化,也有直接被攻击的可能性。

结尾

自此我们完成了RMI的全流程,感叹一句这个过程真的是又臭又长且各种封装,这也是目前我调过最糟心的代码了。写到后面可能也敷衍了些可能看客看的云里雾里的。可以去参考下参考文章的那几篇,写的非常好,我也从中学习了很多甚至直接copy了不少,不过最重要的还是自己完整地调试过全流程,这样对整个过程的把握可以清晰很多。

参考文章

RMI反序列化漏洞之三顾茅庐-流程分析

源码层面梳理Java RMI交互流程

Java反序列化之RMI专题01-RMI基础