RMI基础

S1naG0u Lv2

本文全面介绍了Java RMI(远程方法调用)的基础知识,包括RMI的工作原理、Stub和Skeleton概念、注册中心机制,以及详细的源码分析和安全漏洞利用链的构造方法。

RMI简述

正常的一般情况下java方法调用指的是同一个jvm内方法的调用,RMI 允许一个 Java 虚拟机(JVM)中的对象调用另一个 JVM 中对象的方法,如同调用本地方法

其实也就是服务端将一个远程对象开放出去,客户端可以请求这个服务端,然后使用这个远程对象的方法。

RMI使用socket链接服务端和客户端,每个远程对象对应一个端口,当存在多个远程对象时,就需要一个注册中心来进行管理,客户端向注册中心询问需要的远程对象的ip和端口来实现远程方法调用。

以下是一张模式图:

来自:文章 - JAVA安全基础(四)– RMI机制 - 先知社区

这里有俩个重要的概念:

  1. Stub

    Stub由服务端创建,通过动态代理的方式,动态代理远程对象,包含了远程对象及其IP,端口等信息。在客户端访问服务端时,服务端将Stub发送给客户端,客户端利用接受到的Stub中的网络信息与客户端进行通信

  2. Skeleton
    Skeleton创建于服务端,用来处理由客户端发来的请求

一个简单的RMI实现

当实现RMI时,客户端和服务端需要创建一个相同的要调用的远程方法的接口,并且继承Remote类同时接口方法要抛出RemoteException异常

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

RMIServer

在RMIServer中对上述的接口进行实现,同时要继承UnicastRemoteObject类

1
2
3
4
5
6
7
8
9
10
11
public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj{
protected RemoteObjImpl() throws RemoteException {
}

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

最后创建远程对象,创建注册中心,并且将远程对象绑定到注册中心去

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

RMIClient

客户端就比较简单,链接注册中心,获取远程对象,调用远程方法

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");
remoteObj.sayHello("hello");
}
}

源码分析

1.服务端创建远程服务

1
RemoteObjImpl remoteObj = new RemoteObjImpl();

我们下断点调试一下,这里第一步来到RemoteObjImpl的构造方法。

1
protected RemoteObjImpl() throws RemoteException {}

由于RemoteObjImpl继承了UnicastRemoteObject,我们继续往下跟。

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

这里传入了一个0作为port,然后调用了一个 ==exportObject()==

  • 这个方法会做以下几件事:
  1. 生成Stub(代理)对象:为你的远程对象生成一个代理(Stub),客户端通过Stub与服务端通信。
  2. 监听端口:在指定端口(0表示自动分配)上监听远程调用请求。
  3. 注册到RMI运行时:将该对象注册到RMI运行时,使其可以被远程访问。
  4. 分配对象ID:为远程对象分配唯一的对象标识。

我们继续往下跟

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

这里会创建一个UnicastServerRef(),这个UnicastServerRef就是RMI框架中的一个核心类,主要作用是管理远程对象的导出监听端口处理远程调用请求,以下是其构造过程:

UnicastServerRef

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

这里又new了一个LiveRef()

LiveRef 主要负责:

  • 记录远程对象的唯一标识(ObjID)
  • 记录监听的端口号
  • 记录主机地址
  • 管理底层的“传输层”连接

如下是LiveRef的构造过程:

1
2
3
4
5
6
7
8
9
10
11
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.getLocalEndpoint(port) 为远程对象生成一个“本机IP+端口”的网络身份标识,后续所有RMI通信都要用到它。这里我们就不过多展开了,只需要知道它这里本质就是返回了一个ip+端口。

到这里UnicastServerRef()完成了构造,值得一提的是在 JDK 1.5 之后 UnicastServerRef 里直接包含了 Skeleton 的功能。

我们继续看 exportObject() 方法做了什么

exportObject()

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

这里将上面创建好的UnicastServerRef实例传递给sref变量然后调用sref的exportObject()方法,也就是调用UnicastServerRef.exportObject()

这里由于信息较多我们直接使用注释来说明每句代码的作用

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
public Remote exportObject(Remote impl, Object data, boolean permanent) throws RemoteException
{
//获取远程对象的Class对象,用于后续创建代理和获取方法信息
Class<?> implClass = impl.getClass();
Remote stub;

try {
//使用动态代理创建Stub对象
//第一个参数是远程对象的Class对象
//第二个参数是我们之前创建的liveRef实例,包含了网络请求的功能
//第三个参数表示是否强制使用Stub
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
}
//异常检测
catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
//创建Skeleton对象,这里关系到注册中心,创建远程服务时不会执行,我们后面再了解
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}
//创建Target对象
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
//LiveRef.exportObject()->TCPEndpoint.exportObject()->TCPTransport.exportObject()将封装了全部信息的Target导出到网络
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
}

这里来解释一下这个target的作用,target对之前创建的实例进行了封装,通过构造方法来看:

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

Target存储了远程对象的所有信息,包括远程对象的Class对象,Stub,以及ID等信息。

之后就要真正地将之前创建的所有东西导出到网络,其实也就是将封装了所有内容的Target导出到网络。

这里走了一个LiveRef.exportObject(target)->TCPEndpoint.exportObject(target)->TCPTransport.exportObject(target),我们直接来看最后的TCPTransport.exportObject(target)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void exportObject(Target target) throws RemoteException {
//1. 开启网络监听
synchronized (this) {
listen();//开启网络服务,监听网络请求
exportCount++;//增加导出计数
}

//2. 将Target添加到导出对象表
boolean ok = false;
try {
//这里实质是将Target存储到ObejctTable对象的一个表中
super.exportObject(target);
ok = true;
} finally {
if (!ok) {
synchronized (this) {
//存储失败就取消计数,也就是发布失败
decrementExportCount();
}
}
}
}

这里走了一个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
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);
}
}
}

可以看到这里创建了一个ServerSocket,同时创建了一个线程来进行网络通信,这里就将远程对象发布出去同时处理网络请求,具体的网络请求逻辑我们就不深究了。

到这里就成功创建了远程服务。总结其流程就是:

  1. 调用 RemoteObjImpl 构造函数
    RemoteObjImpl remoteObj = new RemoteObjImpl();
  2. 调用父类 UnicastRemoteObject 构造函数
    super();
  3. 调用带端口的构造函数
    this(0);
  4. 调用 exportObject 方法
    exportObject((Remote) this, port);
  5. 创建动态代理(Stub)
    stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
  6. 创建 Target 对象
    Target target = new Target(impl, this, stub, ref.getObjID(), permanent);
  7. 导出 Target 对象
    ref.exportObject(target);
  8. 确保服务器套接字监听
    listen();
  9. 注册到对象表
    super.exportObject(target);
  10. 完成导出

这里在完成导出后还会执行一步记录的过程,也就是TCPTransport.exportObject(target)中的super.exportObject(target);

1
2
3
4
public void exportObject(Target target) throws RemoteException {
target.setExportedTransport(this);
ObjectTable.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
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();
}
}
}
}

这里objTable.put(oe, target); implTable.put(weakImpl, target);将远程对象的target信息存储起来。到这里全部流程结束

接下来就要进行创建注册中心+绑定

2. 服务端创建注册中心+绑定

创建注册中心

创建注册中心其实和创建远程服务类似,同样是创建注册中心对象,然后将注册中心对象发布到网络,最后返回返回一个创建好的注册中心以供使用

1
Registry registry = LocateRegistry.createRegistry(1099);

跟进去看

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

这里先走Registry的构造方法

在创建动态代理时,Java 使用 Instrumentation 机制来转换类,添加网络通信相关的代码,确保生成的代理类符合安全要求,所以会先走下面这样一段代码,这里不是特比想关我们可以不看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private byte[]
transform( ClassLoader loader,
String classname,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer,
boolean isRetransformer) {
TransformerManager mgr = isRetransformer?
mRetransfomableTransformerManager :
mTransformerManager;
if (mgr == null) {
return null; // no manager, no transform
} else {
return mgr.transform( loader,
classname,
classBeingRedefined,
protectionDomain,
classfileBuffer);
}
}

来到 RegistryImpl() 的构造方法

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
public RegistryImpl(int port)
throws RemoteException
{
// 1. 检查是否是默认端口且设置了安全管理器(其实就是检查是否是默认的1099端口并且通过了安全检查,如果是就不需要安全检查可以特权创建)
if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
// 2. 使用特权操作创建注册中心
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
// 3. 创建 LiveRef
LiveRef lref = new LiveRef(id, port);
// 4. 设置服务端引用
setup(new UnicastServerRef(lref));
return null;
}
}, null, new SocketPermission("localhost:"+port, "listen,accept"));
} catch (PrivilegedActionException pae) {
throw (RemoteException)pae.getException();
}
} else {
// 5. 如果不是默认端口或没有安全管理器,直接创建(使用其他端口或没有通过检查就需要普通创建)
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref));
}
}

这里我们正常是直接走到5这一步了,使用也就不研究上面的安全检查的内容了。

这里首先先创建了一个LiveRef然后又用UnicastServerRef封装了LiveRef,和创建远程服务处的几乎相同,区别就是这里给出了端口,我们往下看setup()方法

1
2
3
4
5
6
7
8
9
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.exportObject() 区别是创建远程对象时第三个参数是false,这里的第三个参数是true,所以我们要关注一下这里。

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

第一个区别出现在stub = Util.createProxy(implClass, getClientRef(), forceStubUse);我们进去看

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

/* REMIND: private remote interfaces? */

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


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

创建远程对象时if (forceStubUse || !(ignoreStubClasses || !stubClassExists(remoteClass)))不满足这个判断的!stubClassExists(remoteClass)条件,所以直接走下面创建了一个动态代理,创建注册中心时是可以满足的,于是走到了createStub(remoteClass, clientRef);里

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
所以这里的区别就是:对于创建 Stub 创建远程服务时是直接通过动态代理创建的,创建注册中心是通过forname创建的。

继续往下看exportObject()方法

第二个区别是if (stub instanceof RemoteStub)判断这里会通过,会走一步 setSkeleton(impl);

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

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

这里同样的话是通过反射创建了一个Skeleton

为什么之前创建注册中心的时候没有这一步呢,之前也有提过,在 JDK 1.5 之后,UnicastServerRef内集成了Skeleton的内容,所以在创建远程服务时在创建Stub通过动态代理同时也直接创建好了Skeleton,而创建注册中心时是利用反射创建的,注册中心的源码在JDK 1.5 之前就已经写好了,是不具备Skeleton的,所以这里在创建Stub后需要同时创建一个Skeleton。

之后的流程就和创建远程服务的流程一样,创建一个Target,然后将注册中心发布到网络,最后在ObejctTable对象的一个表中保存注册中心的信息。

绑定

1
registry.bind("remoteObj",remoteObj);

直接进去看这个方法

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

首先进行一个安全检查(大意是检查调用者是否有权限执行绑定操作),这里无关紧要我们直接跳过,之后从bindings表里面获取一下要绑定的远程对象的name,如果表里面没有,就将远程对象的name和远程对象put到bindings表里面完成绑定。

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

客户端创建代理注册中心

1
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
1
2
3
4
5
public static Registry getRegistry(String host, int port)
throws RemoteException
{
return getRegistry(host, port, null);
}
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
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);
}

可以看到这里其实就是创建了一个注册中心,区别在于这里new LiveRef时最后一个参数isLocal变成了false,同时导致后续的几个参数发生了改变,但最后生成的其实还是一个注册中心,只不过此时其从监听远程链接的行为变成了进行远程链接。

这里实质就是创建了一个给定了ip和端口的代理注册中心对象,由于给出了ip和端口这个代理对象包含了如何与远程注册中心通信的所有必要信息,注意这里并没有进行网络活动,只是创建好了一个代理对象,等待后续的操作。

客户端远程调用注册中心获取远程对象

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

由于服务端实质是将一个动态代理开放到网络中了,所以这里的操作本质上是获取动态代理并返回

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

这里由于源码的版本是 java1.1 我们的版本是1.8所以无法进行动调,只能静态的看。

这里首先调用了newCall(),这里与注册中心开启了一个连接,通过这个连接传输调用信息。

然后var3.writeObject(var1);将传入的字符串信息序列化后利用创建好的连接传给注册中心,所以注册中心会进行==反序列化==,这里就可以存在漏洞,这里我们到服务端再看。

之后来到super.ref.invoke(var2);,这里的super.ref就是UnicastRef,所以就是UnicastRef.invoke(var2)

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
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.
*/
}

这里主要是call.executeCall();下面都是catch了

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

这个方法用来处理返回的数据,主要和网络相关,我们不需要特别深入研究,不过值得注意的是

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

这里当产生 TransportConstants.ExceptionalReturn 这个异常时会通过==反序列化==获取网络传输流里面的对象,如果注册中心返回一个恶意对象,即可能产生漏洞。

也就是说,只要存在 super.ref.invoke() 的结构就可能会导致反序列化漏洞产生。

继续往下看lookup方法

1
2
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();

这里==反序列化==了从远程注册中心传回来的输入流,当构造一个恶意的注册中心时可能会导致产生漏洞。

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

1
remoteObj.sayHello("hello");

这里由于得到的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);
}
}

前面的if都是安全检查类的东西,直接来看 invokeRemoteMethod(proxy, method, args)

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
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(),不过由于参数不同此处并不是上述的那个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
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);
}
}
}

这里代码有点庞大,我们挑主要的看

首先是会调用一个

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
marshalValue(types[i], params[i], out);

protected static void marshalValue(Class<?> type, Object value,
ObjectOutput out)
throws IOException
{
if (type.isPrimitive()) {
if (type == int.class) {
out.writeInt(((Integer) value).intValue());
} else if (type == boolean.class) {
out.writeBoolean(((Boolean) value).booleanValue());
} else if (type == byte.class) {
out.writeByte(((Byte) value).byteValue());
} else if (type == char.class) {
out.writeChar(((Character) value).charValue());
} else if (type == short.class) {
out.writeShort(((Short) value).shortValue());
} else if (type == long.class) {
out.writeLong(((Long) value).longValue());
} else if (type == float.class) {
out.writeFloat(((Float) value).floatValue());
} else if (type == double.class) {
out.writeDouble(((Double) value).doubleValue());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
out.writeObject(value);
}
}

这里大意的话就是把我们远程调用方法时传入的参数传给服务端,如果不是基础的类型的话就会使用==序列化==传

然后继续往下走会调用一个 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
Object returnValue = unmarshalValue(rtype, in);
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();
}
}

这里是读取从服务端传回的内容,这里如果有一个恶意的远程对象也会受==反序列化==攻击。

JRMP攻击

executeCall()方法执行的就是JRMP协议,所以常说的JRMP攻击其实就是此处产生的

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

这里我们跟一个调用链一直走:

LocateRegistry.createRegistry() -> RegistryImpl() -> setup() -> UnicastServerRef.exportObject() -> UnicastServerRef.exportObject() -> LiveRef.exportObject() -> TCPEndpoint.exportObject() -> listen() -> AcceptLoop() -> run() -> executeAcceptLoop() -> ConnectionHandler() -> run() -> run0() -> handleMessages() -> 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
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
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;
}

这里serviceCall 方法是 Java 框架中服务端用于处理客户端远程调用请求的核心方法,他主要有如下作用:

  1. 读取客户端请求中的对象ID(ObjID)。
  2. 查找并获取对应的远程对象(Remote Object)。
  3. 调用该对象的分发器(Dispatcher)来实际执行远程方法。
  4. 处理调用过程中的各种异常,并将异常信息返回给客户。

这里我们主要看

1
disp.dispatch(impl, call);

这里的disp是UnicastServerRef

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

这里会走到一个oldDispatch()方法

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

这里会执行:

1
skel.dispatch(obj, call, op, hash);

到这里终于走到了我们的核心类 注册中心的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
    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);
}
......

这里的这个case2中存在一个readObject,其原本的作用是用来处理来自客户端

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

处的字符串,从安全的角度出发这里也可能存在反序列化漏洞

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

这里前半部分和上一环节中的内容一模一样,到 UnicastServerRef.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
    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);
}
......

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

这里由于skel是空所以会继续往下走,到

1
params[i] = unmarshalValue(types[i], in);
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
remoteObj.sayHello("hello");

将客户端传来的这个参数反序列化读出,由于使用了readObject() 方法,这里也会导致产生反序列化漏洞。

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

一开头会执行一个if,这里其实会创建一个和远程服务结构类似的DGC,由于调用的是DGCImpl.dgcLog的方法,是一个静态属性,参考java类加载机制进行类的初始化,这里会执行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
    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);
......

这里有点类似于注册中心那里Util.createProxy(DGCImpl.class,new UnicastRef(ref), true);->stubClassExists(remoteClass)

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.class,其中也会调用 super.ref.invoke() 同客户端请求服务端-客户端那里一样,所以此处也会导致JRMP攻击,还有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
......
}

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

这里的try里面也会执行一个readObject()所以也可能会受到反序列化攻击。所以DCC客户端同样可能会受到反序列化攻击,同样DGC也存在skel

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

这里同样有许多的 readObject() 方法,因此DGC的服务端同样也存在反序列化漏洞攻击点。

总结

通过对RMI源码的深入分析,我们发现了RMI系统中存在的多个反序列化漏洞攻击点。以下是具体的攻击点和攻击方向:

主要攻击点

1. 注册中心攻击点

  • 恶意注册中心 → 客户端: 注册中心返回恶意对象,客户端反序列化时触发漏洞
  • 恶意客户端 → 注册中心: 客户端发送恶意请求,注册中心反序列化时触发漏洞

2. JRMP协议攻击点

  • 恶意服务端 → 客户端: 服务端返回恶意异常对象,客户端反序列化时触发漏洞
  • 恶意客户端 → 服务端: 客户端发送恶意调用,服务端反序列化时触发漏洞

3. DGC垃圾回收攻击点

  • 恶意DGC服务端 → DGC客户端: DGC服务端返回恶意对象,DGC客户端反序列化时触发漏洞
  • 恶意DGC客户端 → DGC服务端: DGC客户端发送恶意请求,DGC服务端反序列化时触发漏洞

攻击特征

  • 核心原理: 利用 readObject() 方法进行反序列化
  • 关键结构: super.ref.invoke() 是JRMP攻击的标志
  • 触发条件: 网络通信过程中的数据传输
  • 标题: RMI基础
  • 作者: S1naG0u
  • 创建于 : 2025-03-25 20:53:01
  • 更新于 : 2025-08-12 17:23:28
  • 链接: https://s1nag0u.github.io/2025/03/25/RMI基础/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。