RMI基础

RMI专题

RMI简介

最早的最早,从分布式概念出现以后,工程师们,制造了一种,基于Java语言的远程方法调用的东西,它叫RMI(Remote Method Invocation),我们使用Java代码,可以利用这种技术,去跨越JVM,调用另一个JVM的类方法。

因为任何东西都是基于socket,RMIClient直接去找RMIServer,并不知道这个类是基于哪个端口的,所以有了RMI Registry

我们需要把被调用的类,注册到一个叫做RMI Registry的地方,只有把类注册到这个地方,调用者就能通过RMI Registry找到类所在JVM的ip和port,才能跨越JVM完成远程方法的调用。

实现

这里我们来做一个简单的实现

我们需要准备一个Client和一个Server,首先他们需要定义一个一样的接口IRemoteObj

1
2
3
4
5
6
7
8
9
package com.source;

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

public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}

服务端

服务端需要去把这个接口实现,因为最后调用的是服务端的代码

RemoteObjlmpl这个类在定义的时候有要求,需要继承UnicastRemoteObject这个类(如果你想把这个绑定到RMIRegistry中,就需要去继承它

1
2
3
4
5
6
7
8
9
10
11
public class RemoteObjlmpl extends UnicastRemoteObject  implements IRemoteObj{

public RemoteObjlmpl() throws RemoteException {}
@Override
public String sayHello(String keywords){
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}

在服务端代码中,首先我们需要去创建一个远程对象来保证通信,但是客户端并不知道这个端口是多少,所以需要一个注册中心RMIregistry,它是有固定端口的,一般是1099,最后把注册中心绑定到远程对象上

1
2
3
4
5
6
7
public class RMIServer {
public static void main(String[] args) throws Exception {
IRemoteObj remoteObj = new RemoteObjlmpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("remoteObj",remoteObj);
}
}

客户端

客户端获取注册中心(ip和端口都是固定的),从注册中心中查找remoteObj远程对象,然后去调用他的方法
1
2
3
4
5
6
7
public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

在这通信过程中,通过网络操作去实现一个内存操作,这里面有一个对象的创建和调用过程,这里面全是通过序列化和反序列化实现的

源码层面分析

创建远程服务

创建远程服务代码如下,我们动态调试来看一下
1
IRemoteObj remoteObj = new RemoteObjlmpl();

首先会调用RemoteObjImpl的构造方法

由于这个类继承于UnicastRemoteObject,使用会调用父类的无参构造

在其父类的构造方法中,会将默认port设置为0(后续会随机设置一个端口)

1
2
3
4
5
6
7
8
9
10
protected UnicastRemoteObject() throws RemoteException
{
this(0);
}

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

之后会有一系列的赋值,我们就不细看了

在其构造方法中,会调用exportObject,它的名字就是”导出对象“,很明显是我们的核心函数

其中,它创建了一个UnicastServerRef,”服务端引用“,其中传入了一个端口,前面这个obj,很明显是我们用来实现逻辑的,而后面这个对象,就是我们用来处理网络请求的了

1
2
3
4
5
public static Remote exportObject(Remote obj, int port)
throws RemoteException
{
return exportObject(obj, new UnicastServerRef(port));
}

在UnicastServerRef的构造函数中,创建了一个LiveRef类(非常重要的一个类),将port传入其构造函数

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

(其中传入的一个ObjID我们就不多说了)其中TCPEndpoint的构造函数(一个IP,一个端口),很明显就是用来处理网络请求的

LiveRef中,存着endpoint,objID与isLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public LiveRef(int port) {
this((new ObjID()), port);
}

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

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

//TCPEndpoint
public TCPEndpoint(String host, int port) {
this(host, port, null, null);
}

其中ep存入的东西如下(目前port为0),后面我们会说到,transport才是真正处理网络请求的东西,外面的每一层都是对它的封装

这样LiveRef就创建好了,然后会调用它父类的构造函数

在其父类的构造函数,也是简单赋了值

1
2
3
4
5
6
7
public UnicastServerRef(int port) {
super(new LiveRef(port));
}

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

返回回去,我们进入exportObject方法,在sref中,封装着LiveRef

在这段中,就是一直在不同的类中调用exportObject

1
2
3
4
5
6
7
8
9
10
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);
}
}

进入sref中的exportObject方法,在这里创建了代理stub

那么有人问了,我这里不是服务端吗,为什么会创建客户端的stub呢?

这里是因为,是服务端创建stub,将stub放在注册中心,客户端去注册中心拿到stub,然后用stub去操作skeleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}

我们看看createProxy中的逻辑,其中implClass中放的是远程对象的类,clientRef中放着LiveRef

其中有一个判断,forceStubUse和ignoreStubClasses咱们暂时不解释它,看一下stubClassExists中的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
Class<?> remoteClass;

...

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

...
}

它判断,如果存在 远程类的名字+ “_Stub” 这个类,他就会走进去

实际上JDK中有一些类是已经定义好的(现在我们先过,后面会说)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

还是在这个类中,接下来就会创建动态代理,经过些,动态代理就已经创建好了

1
2
3
4
5
6
7
8
9
10
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);
}

tip:在创建代理过程中的几个参数如下

若上述过程中,存在这么一个系统内置的stub类,就会进入下面的if当中(后续讨论)

1
2
3
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

创建完成后,会将当前有用的信息封装进入,就是一个总封装(其中封装的信息如下)

1
2
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);

接下来就把我们封装的东西发布出去(使用exportObject函数),我们进入看一下它的逻辑

1
ref.exportObject(target);

这里会层层调用exportObject函数,最终走到了TCPTransport的exportObject,这里是最终处理网络请求的地方,在TCPTransport#exportObject会调用listen函数,这里就会开放端口,我们跟进去看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//LiveRef#exportObject
public void exportObject(Target target) throws RemoteException {
ep.exportObject(target);
}

// TCPEndpoint#exportObject
public void exportObject(Target target) throws RemoteException {
transport.exportObject(target);
}

//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();
}
}
}
}

server = ep.newServerSocket();构造了一个socket,创建一个线程后打开线程,等待别人来连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// TCPTransport#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);
}
}
}

在构造socket的过程中,如果说端口是0的话,就会随机设置一个端口

1
2
if (listenPort == 0)
setDefaultPort(server.getLocalPort(), csf, ssf);

最后经过一系列返回,最后ObjectTable中,使用putTarget将target保存在了一个静态表objTable中

1
2
3
...
objTable.put(oe, target);
implTable.put(weakImpl, target);

创建服务端的代码就走完了,最后等待连接

创建注册中心+绑定

创建注册中心

tip:表面上注册中心和远程服务是不一样的东西,但是实际上是一样的东西

创建注册中心代码如下,我们将1099默认端口传入

1
Registry r = LocateRegistry.createRegistry(1099);

走进createRegistry方法中,该方法会创建RegistryImpl对象

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

我们看一下它的构造方法,首先开头的if判断是一个安全验证的东西,我们暂时不看,在下方else中,会创建LiveRef对象和UnicastServerRef对象,UnicastServerRef中放入LiveRef(和我们之看的创建远程服务很是类似)最后调用了一个setup方法

1
2
3
4
5
6
7
8
9
10
public RegistryImpl(int port)
throws RemoteException
{
if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
...
} else {
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref));
}
}

看看再setup方法中干了些什么

与注册远程服务时UnicastRemoteObject#exportObject相比,只有第三个参数由false变为了ture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}

// UnicastRemoteObject#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);
}

boolean permanent为第三个参数,意为永久性,所以说我们现在创建的注册中心是一个永久性的对象,而之前说的远程服务是一个临时性的对象

1
2
public Remote exportObject(Remote impl, Object data,
boolean permanent)

接下来我们应该可以回顾起来之前的知识

下面我把有区别的地方拿出来,再最后调用的stubClassExists中的if判断时候,最终会找到RegistryImpl_Stub类,所以会走到try中的函数并返回true

createProxy的if判断中返回true后,会走到if中,执行createStub函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// UnicastServerRef#exportObject
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
...
try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
...
}

// Util#createProxy
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
...
if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}
...
}

// Util#stubClassExists
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;
}

createStub函数中也非常简单,将我们所找到的类先初始化,再实例化出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref)
throws StubNotFoundException
{
String stubname = remoteClass.getName() + "_Stub";

/* Make sure to use the local stub loader for the stub classes.
* When loaded by the local loader the load path can be
* propagated to remote clients, by the MarshalOutputStream/InStream
* pickle methods
*/
try {
Class<?> stubcl =
Class.forName(stubname, false, remoteClass.getClassLoader());
Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
return (RemoteStub) cons.newInstance(new Object[] { ref });

} catch (ClassNotFoundException e) {
throw new StubNotFoundException(
"Stub class not found: " + stubname, e);
} catch (NoSuchMethodException e) {
throw new StubNotFoundException(
"Stub class missing constructor: " + stubname, e);
} catch (InstantiationException e) {
throw new StubNotFoundException(
"Can't create instance of stub class: " + stubname, e);
} catch (IllegalAccessException e) {
throw new StubNotFoundException(
"Stub class constructor not public: " + stubname, e);
} catch (InvocationTargetException e) {
throw new StubNotFoundException(
"Exception creating instance of stub class: " + stubname, e);
} catch (ClassCastException e) {
throw new StubNotFoundException(
"Stub class not instance of RemoteStub: " + stubname, e);
}
}

创建完成之后的Stub是一个RegistryImpl_Stub,其中封装着UnicastRefUnicastRef封装着LiveR``ef

由于我们的Stub是RegistryImpl_Stub,是JDK中自带的Stub,而它是继承于RemoteStub的,所以会进入到if当中,执行setSkeleton方法

1
public final class RegistryImpl_Stub extends RemoteStub implements Registry, Remote 
1
2
3
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

setSkeleton方法中,会创建一个Sekleton,服务端和客户端都会有代理来处理网络请求,客户端代理为Stub,而服务端代理则为Sekleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}
}
}

最后也是直接通过 远程类的名字+ “_Skel” 初始化并且实例化这个类并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
}
}

最后也是,创建一个Target,去吧有用的东西都存入进去,并把我们封装的东西发布出去(使用exportObject函数)

1
2
3
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);

最后在TCPTransport中的exportObject方法中调用其父类的exportObject,使用putTarget方法,将所封装的Target放入静态表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// TCPTransport#exportObject
public void exportObject(Target target) throws RemoteException {
...
try {
super.exportObject(target);
ok = true;
} ...
}

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

// ObjectTable#putTarget
static void putTarget(Target target) throws ExportException {
...
objTable.put(oe, target);
implTable.put(weakImpl, target);

...
}

在静态表中,一共有三个Stub,其中一个是DGCImpl_Stub(分布式垃圾回收,默认创建),可以在表中点着看看,实际上其实和远程服务是一个东西

这就是创建注册中心的流程

绑定

相比于其他的,这个绑定流程就非常简单了
1
r.bind("remoteObj",remoteObj);

进入到bind方法中

其中checkAccess方法是检查是否在本地绑定

bindings是一个静态表,bindings.get是从静态表中寻找name的,如果说表中存在该name的绑定,则会抛出一个AlreadyBoundException的异常,如果没有的话,就会将(name,远程对象)put进去。

1
2
3
4
5
6
7
8
9
10
11
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);
}
}

tips:在RMI的实现上,要求注册中心和服务端在同一台主机上,低版本时实现上允许远程绑定,导致一些漏洞

客户端请求注册中心-客户端

客户端会做两件事情,第一个就是向注册中心去拿远程对象的代理,第二个对服务端进行一个调用

有人会觉得,我获取一个远程对象,肯定要有序列化和反序列化,但实际却不太一样

1
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

实际,我们将ip和端口传入,他首先是封装了一个LiveRef,然后调用Util.createProxy方法

和我们之前看到的创建流程好像一样,其实是在本地创建了一个,我们就有了注册中心的stub对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// LocateRegistry#getRegistry
public static Registry getRegistry(String host, int port)
throws RemoteException
{
return getRegistry(host, port, null);
}
// LocateRegistry#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 = "";
}
}
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);
}

下一步就是去查找远程对象,客户端将名字给过去,获取到一个远程对象的代理

1
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");

我们进入lookup方法看一下,首先它将我们传入的那个名字(字符串),写入了一个输出流,就是序列化进去了,后续肯定还有一个反序列化的点

后面还有一个读输入流的地方,将输入流读出来的进行反序列化,赋值到var23上,我们可以知道,这个var23就是我们从注册中心获取回来的stub

其中对从注册中心读到的输入流进行反序列化的这个地方,可能就存在攻击点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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.invoke(var2);调用了UnicastRefinvoke方法,它调用了executeCall方法,这个方法是用来处理网络请求的

1
2
3
4
5
6
7
8
public void invoke(RemoteCall call) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");

call.executeCall();

} ...
}

这里还存在一个攻击点,就在StreamRemoteCallexecuteCall方法中

如果返回异常,且异常为TransportConstants.ExceptionalReturn,就会将这个异常反序列化出来,如果说注册中心返回一个恶意的流,就会导致客户端被攻击

这里的攻击面是非常广的,只要调用了StreamRemoteCall#executeCall或者说UnicastRef#invoke,就会有攻击面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public void executeCall() throws Exception {
...

// 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");
}
}

客户端请求服务端-客户端

我们来看一下客户端请求服务端,客户端是怎么做的
1
System.out.println(remoteObj.sayHello("SheepSean"));

第一步,我们会走到一个非预期的地方,因为remoteObj是一个动态远程代理,调用它的任何方法,都会跳到invoke方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
}

最后它走到了invokeRemoteMethod中,在其中,我们要看一下ref.invoke方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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) {
......
}
}

UnicastRef#invoke中,仍然调用了call.executeCall(),我们知道,只要与网络请求有关的就会调用它,这里会有一个攻击点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
public Object invoke(Remote obj,
Method method,
Object[] params,
long opnum)
throws Exception
{
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "method: " + method);
}

if (clientCallLog.isLoggable(Log.VERBOSE)) {
logClientCall(obj, method);
}

Connection conn = ref.getChannel().newConnection();
RemoteCall call = null;
boolean reuse = true;

/* If the call connection is "reused" early, remember not to
* reuse again.
*/
boolean alreadyFreed = false;

try {
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "opnum = " + opnum);
}

// create call context
call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum);

// marshal parameters
try {
ObjectOutput out = call.getOutputStream();
marshalCustomCallData(out);
Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
marshalValue(types[i], params[i], out);
}
} catch (IOException e) {
clientRefLog.log(Log.BRIEF,
"IOException marshalling arguments: ", e);
throw new MarshalException("error marshalling arguments", e);
}

// unmarshal return
call.executeCall();

try {
Class<?> rtype = method.getReturnType();
if (rtype == void.class)
return null;
ObjectInput in = call.getInputStream();

/* StreamRemoteCall.done() does not actually make use
* of conn, therefore it is safe to reuse this
* connection before the dirty call is sent for
* registered refs.
*/
Object returnValue = unmarshalValue(rtype, in);

/* we are freeing the connection now, do not free
* again or reuse.
*/
alreadyFreed = true;

/* if we got to this point, reuse must have been true. */
clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");

/* Free the call's connection early. */
ref.getChannel().free(conn, true);

return returnValue;

} catch (IOException e) {
clientRefLog.log(Log.BRIEF,
"IOException unmarshalling return: ", e);
throw new UnmarshalException("error unmarshalling return", e);
} catch (ClassNotFoundException e) {
clientRefLog.log(Log.BRIEF,
"ClassNotFoundException unmarshalling return: ", e);

throw new UnmarshalException("error unmarshalling return", e);
} finally {
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.
*/
reuse = false;
}
}

} catch (RuntimeException e) {
/*
* Need to distinguish between client (generated by the
* invoke method itself) and server RuntimeExceptions.
* Client side RuntimeExceptions are likely to have
* corrupted the call connection and those from the server
* are not likely to have done so. If the exception came
* from the server the call connection should be reused.
*/
if ((call == null) ||
(((StreamRemoteCall) call).getServerException() != e))
{
reuse = false;
}
throw e;

} catch (RemoteException e) {
/*
* Some failure during call; assume connection cannot
* be reused. Must assume failure even if ServerException
* or ServerError occurs since these failures can happen
* during parameter deserialization which would leave
* the connection in a corrupted state.
*/
reuse = false;
throw e;

} catch (Error e) {
/* If errors occurred, the connection is most likely not
* reusable.
*/
reuse = false;
throw e;

} finally {

/* alreadyFreed ensures that we do not log a reuse that
* may have already happened.
*/
if (!alreadyFreed) {
if (clientRefLog.isLoggable(Log.BRIEF)) {
clientRefLog.log(Log.BRIEF, "free connection (reuse = " +
reuse + ")");
}
ref.getChannel().free(conn, reuse);
}
}
}

还有一个地方,这个方法会将传入的参数传入marshalValue中,这里面是进行序列化操作的,那么有序列化,肯定还有反序列化,就在executeCall后的unmarshalValue方法内,将传回来的参数进行反序列化,这里就又存在一个攻击点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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();
}
}

客户端请求注册中心-注册中心

接下来我们看看当客户端请求注册中心的时候,注册中心做了一些什么样的操作
1
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");

服务端调用Skel,客户端调用Stub,所以断点应该下载RegistryImpl_Skel类中,我们也是需要看下到底是怎么调用到RegistryImpl_Skel中的

我们走到之前的listen方法处,listen方法创建了一个新的线程,主要需要去看AcceptLoop里面的run方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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);
}
......
}

在AcceptLoop的run方法中,其实并没有什么东西,只有一个executeAcceptLoop方法,走入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void run() {
try {
executeAcceptLoop();
} finally {
try {
/*
* Only one accept loop is started per server
* socket, so after no more connections will be
* accepted, ensure that the server socket is no
* longer listening.
*/
serverSocket.close();
} catch (IOException e) {
}
}
}

在executeAcceptLoop方法中,创建了新的线程ConnectionHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void executeAcceptLoop() {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "listening on port " +
getEndpoint().getPort());
}

while (true) {
Socket socket = null;
try {
socket = serverSocket.accept();

/*
* Find client host name (or "0.0.0.0" if unknown)
*/
InetAddress clientAddr = socket.getInetAddress();
String clientHost = (clientAddr != null
? clientAddr.getHostAddress()
: "0.0.0.0");

/*
* Execute connection handler in the thread pool,
* which uses non-system threads.
*/
try {
connectionThreadPool.execute(
new ConnectionHandler(socket, clientHost));
} catch (RejectedExecutionException e) {
closeSocket(socket);
tcpLog.log(Log.BRIEF,
"rejected connection from " + clientHost);
}

}
......
}
}

我们看TCPTransport的run方法,在其被调用时,其方法内部会调用run0方法

其中较为重要的就是handleMessages

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// TCPTransport#run
public void run() {
Thread t = Thread.currentThread();
String name = t.getName();
try {
t.setName("RMI TCP Connection(" +
connectionCount.incrementAndGet() +
")-" + remoteHost);
AccessController.doPrivileged((PrivilegedAction<Void>)() -> {
run0();
return null;
}, NOPERMS_ACC);
} finally {
t.setName(name);
}
}

// TCPTransport#run0
private void run0() {

......
// read input messages
handleMessages(conn, true);
break;

......
}

在handleMessages方法中,op在默认情况下都是TransportConstants.Call,所以最后会调用到serviceCall方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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:
......
}
} while (persistent);

}
}

serviceCall方法中会从静态表中读取处Targert

此时serviceCall中disp是一个RegistryImpl_Skel类,该方法会从target中提取出dispatcher

最后会调用到disp.dispatch方法,进入到RegistryImpl_Skel类的dispatch方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public boolean serviceCall(final RemoteCall call) {
try {
......
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);
}

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

客户端请求服务端-服务端

服务端和注册中心的流程其实一样,当客户端去调用服务端远程方法时,最后也会进入serviceCall方法,调用dispatch方法
1
disp.dispatch(impl, call);

进入dispatch方法后,我们可以看到,当skel为null的时候,才能走到下面的代码,第一个target为DGC,此时skel不为空直接返回

当我们请求到服务端动态代理后,发现skel为空,则不会直接返回而是走到下面的代码。

首先会读取输入流,从输入流中先获取到method,就是我们所调用的sayhello方法

接下来会将我们的参数用unmarshalValue反序列化出来,再调用method.invoke方法去调用我们所调用的sayHello方法,然后会用marshalValue将函数的返回值result序列化进去,将其传回客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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);
}

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

客户端请求服务端-DGC

创建

DGC是RMI中分布式垃圾回收的模块

之前我们看到,在静态表中,除了我们所创建的两个Target,还有一个DGC的Target,DGC这个类实际是在DGCImpl.dgcLog.isLoggable(Log.VERBOSE)这里被创建的,有人可能问,这里不是就简单的一行代码调用吗,我们之前讲过,调用一个类的静态变量时候,是会完成类的初始化的

我们在put Target的地方下一个断点,这里put的Target是一个动态代理类,说明在我们将Target放入静态表的时候,DGC已经被创建好并放入静态表中了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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();
}
}
}
}
1
2
3
static final Log dgcLog = Log.getLog("sun.rmi.dgc", "dgc",
LogStream.parseLevel(AccessController.doPrivileged(
new GetPropertyAction("sun.rmi.dgc.logLevel"))));

在类初始化的时候实际上也是会走到它类中的静态代码块的位置,在静态代码块的中间,实际就new了一个DGCImpl,后续的创建方式,也就和创建注册中心的方法很类似了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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;
}
});
}

在调用createProxy时候,stubClassExists还是会去检查,JDK中有没有DGCImpl_Stub,确实是有这个类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
......

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

......
}

发现存在这个类,就将其实例化并返回true

和注册中心相类似,注册中心的端口用来注册服务,而DGC的端口是用来远程回收服务,只是端口不是确定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

功能

在DGCImpl_Stub中存在两个方法,可以理解为一个比较弱的清理,一个比较干净的清理

下面的两个方法都存在风险点,他们都调用了UnicastRef的invoke方法,我们之前说过,这个方法存是存在风险的

还有一个就在dirty的var9.readObject()处,会进行一个反序列化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public void clean(ObjID[] var1, long var2, VMID var4, boolean var5) throws RemoteException {
try {
RemoteCall var6 = super.ref.newCall(this, operations, 0, -669196253586618813L);

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

super.ref.invoke(var6);
super.ref.done(var6);
} catch (RuntimeException var9) {
throw var9;
} catch (RemoteException var10) {
throw var10;
} catch (Exception var11) {
throw new UnexpectedException("undeclared checked exception", var11);
}
}

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

我们接下来看看服务端DGCImpl_Skel

其中有两个case,应该就是clean和dirty了,其中也是明显的几处反序列化的地方,都存在着攻击点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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");
}

}
}

Java高版本绕过

在Java高版本中,在RegistryImpl类中新加了一个registryFilter方法,里面对所传入的序列化对象的类型进行了限制
1
2
3
4
5
6
7
8
9
10
11
12
if (String.class == clazz
|| java.lang.Number.class.isAssignableFrom(clazz)
|| Remote.class.isAssignableFrom(clazz)
|| java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
|| UnicastRef.class.isAssignableFrom(clazz)
|| RMIClientSocketFactory.class.isAssignableFrom(clazz)
|| RMIServerSocketFactory.class.isAssignableFrom(clazz)
|| java.rmi.server.UID.class.isAssignableFrom(clazz)) {
return ObjectInputFilter.Status.ALLOWED;
} else {
return ObjectInputFilter.Status.REJECTED;
}

其中有希望利用的只有Proxy和UnicastRef类,其中最重要的是UnicastRef类,在这个类中有一个invoke方法 ,在修复以后,客户端的被攻击点是没有被修复的,我们就想如果能让服务端去发送一个客户端请求,就会暴露出攻击点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void invoke(RemoteCall var1) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");
var1.executeCall();
} catch (RemoteException var3) {
clientRefLog.log(Log.BRIEF, "exception: ", var3);
this.free(var1, false);
throw var3;
} catch (Error var4) {
clientRefLog.log(Log.BRIEF, "error: ", var4);
this.free(var1, false);
throw var4;
} catch (RuntimeException var5) {
clientRefLog.log(Log.BRIEF, "exception: ", var5);
this.free(var1, false);
throw var5;
} catch (Exception var6) {
clientRefLog.log(Log.BRIEF, "exception: ", var6);
this.free(var1, true);
throw var6;
}
}

我们的想法是找一个地方去调用Util.createProxy创建一个动态代理类,我们找到的是DGC这个类可以被利用,然后调用它的clean或者dirty方法去触发他的invoke方法

我们直接走向最终找到的DGCClient内部类EndpointEntry的构造方法方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}

接下来我们找哪里去创建了这么一个类,在EndpointEntry中的lookup方法中创建了这么一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
public static EndpointEntry lookup(Endpoint ep) {
synchronized (endpointTable) {
EndpointEntry entry = endpointTable.get(ep);
if (entry == null) {
entry = new EndpointEntry(ep);
endpointTable.put(ep, entry);
if (gcLatencyRequest == null) {
gcLatencyRequest = GC.requestLatency(gcInterval);
}
}
return entry;
}
}

我们看只有在registerRefs中调用了lookup方法

1
2
3
4
5
6
7
8
9
10
11
12
13
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));
}

我们需要找到一个反序列化利用的点,在向上找,会找到两个调用registerRefs的方法

我们看read方法中,如果这个输入流不是并且不继承于ConnectionInputStream的话,就会调用我们的registerRefs方法,但是这个in是一个ConnectionInputStream,所以我们只能去找另一个方法去利用

1
2
3
4
5
6
7
8
9
10
11
12
13
if (in instanceof ConnectionInputStream) {
ConnectionInputStream stream = (ConnectionInputStream)in;
// save ref to send "dirty" call after all args/returns
// have been unmarshaled.
stream.saveRef(ref);
if (isResultStream) {
// set flag in stream indicating that remote objects were
// unmarshaled. A DGC ack should be sent by the transport.
stream.setAckNeeded();
}
} else {
DGCClient.registerRefs(ep, Arrays.asList(new LiveRef[] { ref }));
}

另一个最终的流程是releaseInputStream去调用StreamRemoteCall.registerRefs然后进入到if中(这个判断条件中的incomingRefTable是为空的,我们后续会说怎么走到if里面)调用DGCClient.registerRefs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void releaseInputStream() throws IOException {
try {
if (in != null) {
try {
in.done();
} catch (RuntimeException e) {
}
in.registerRefs();
in.done(conn);
}
conn.releaseInputStream();
} finally {
in = null;
}
}

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

最后调用releaseInputStream的地方就是非常的多了,在许多Skel中都有调用

最后的流程如下,只要调用releaseInputStream,就会创建一个proxy对象

实际上反序列化流程,只是为了给incomingRefTable赋值,攻击流程实际是在正常的调用流程中

我们上面说过,这里的值默认是空的,要想走入DGCClient.registerRefs中,我们就应该去找,哪里给incomingRefTable赋值

1
2
3
4
5
6
7
8
void registerRefs() throws IOException {
if (!incomingRefTable.isEmpty()) {
for (Map.Entry<Endpoint, List<LiveRef>> entry :
incomingRefTable.entrySet()) {
DGCClient.registerRefs(entry.getKey(), entry.getValue());
}
}
}

实际这里只有一个地方ConnectionInputStream的saveRef中,向里面put进去了一个东西,使他不为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void saveRef(LiveRef ref) {
Endpoint ep = ref.getEndpoint();

// check whether endpoint is already in the hashtable
List<LiveRef> refList = incomingRefTable.get(ep);

if (refList == null) {
refList = new ArrayList<LiveRef>();
incomingRefTable.put(ep, refList);
}

// add ref to list of refs for endpoint ep
refList.add(ref);
}

saveRef也是只有一个地方去调用,就是read方法,我们之前讨论过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static LiveRef read(ObjectInput in, boolean useNewFormat)
throws IOException, ClassNotFoundException
{
......

if (in instanceof ConnectionInputStream) {
ConnectionInputStream stream = (ConnectionInputStream)in;
// save ref to send "dirty" call after all args/returns
// have been unmarshaled.
stream.saveRef(ref);
if (isResultStream) {
// set flag in stream indicating that remote objects were
// unmarshaled. A DGC ack should be sent by the transport.
stream.setAckNeeded();
}
} else {
DGCClient.registerRefs(ep, Arrays.asList(new LiveRef[] { ref }));
}

return ref;
}
}

我们看看谁调用了read方法,只有UnicastRef和UnicastRef2中的readExternal去调用了read方法

readExternal是一个和readObject类似但不一样的东西,如果所反序列化的类,也有readExternal方法,也会去调用readExternal方法

1
2
3
4
5
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException
{
ref = LiveRef.read(in, false);
}

UnicastRef是白名单里面的内容,我们向客户端传入一个UnicastRef对象触发它的readexternal方法

1
2
3
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
this.ref = LiveRef.read(var1, false);
}

进入到LiveRef.read中… 剩下的调用我们就不再重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static LiveRef read(ObjectInput var0, boolean var1) throws IOException, ClassNotFoundException {
......
if (var0 instanceof ConnectionInputStream) {
ConnectionInputStream var6 = (ConnectionInputStream)var0;
var6.saveRef(var5);
if (var4) {
var6.setAckNeeded();
}
} else {
DGCClient.registerRefs(var2, Arrays.asList(var5));
}

return var5;
}

...

后续会走到EndpointEntry中,在创建完dgc后会走到下面创建一个RenewCleanThread线程

1
2
3
4
5
6
7
8
9
10
11
12
13
private EndpointEntry(Endpoint var1) {
this.endpoint = var1;

try {
LiveRef var2 = new LiveRef(DGCClient.dgcID, var1, false);
this.dgc = (DGC)Util.createProxy(DGCImpl.class, new UnicastRef(var2), true);
} catch (RemoteException var3) {
throw new Error("internal error creating DGC stub");
}

this.renewCleanThread = (Thread)AccessController.doPrivileged(new NewThreadAction(new RenewCleanThread(), "RenewClean-" + var1, true));
this.renewCleanThread.start();
}

RenewCleanThread中,会调用DGCClient的makeDirtyCall方法,而这个方法最终会调用他的dirty方法,就会调用到invoke方法,最终让服务器发送客户端请求

1
2
3
4
5
6
7
8
9
10
11
12
13
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (var4) {
EndpointEntry.this.makeDirtyCall(var5, var6);
}

if (!EndpointEntry.this.pendingCleans.isEmpty()) {
EndpointEntry.this.makeCleanCalls();
}

return null;
}
}