RMI小记

这期主要就是通过debug,一步步调试来找到RMI的反序列化点,并了解RMI的具体流程。

这部分我是看组长的视频学的,看了两三遍了。下面是视频的网址

https://www.bilibili.com/video/BV1L3411a7ax?p=2&vd_source=6a92aae104751f5bc065a281b15c4fcb

下面是要用到的class

服务端

下面是服务端要用到的代码

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
// testRMIServer.java
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class testRMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
IRemoteObj remoteObj = new RemoteObjImpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("remoteObj", remoteObj);
}
}

// IRemoteObj.java
import java.rmi.Remote;
import java.rmi.RemoteException;

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

// RemoteObjImpl.java
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Locale;

public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
public RemoteObjImpl() throws RemoteException {
//UnicastRemoteObject.exportObject(this, 0); //如果不继承UnicastRemoteObject就需要手工导出
}

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

创建远程服务

先在这打个断点

RMI远程服务1.png

然后先强制步入再步过就会到

RMI远程服务2.png

这是一个静态赋值

我们再步入就会回到原来的testRMIServer,然后我们的这个IRemoteObj这个类有一个父类,接着步入去看,它继承自UnicastRemoteObject。我们再去查看这个UnicastRemoteObject的源码。

RMI远程服务3.png

这个IRemoteObj的静态值都在构造函数之前赋值。然后我们现在是已经有这个RemoteObj,接下来就是分析怎么把这个对象发布到网上去。

再强制步入,进入到RemoteObjImpl的父类的构造函数,再步入就会到下面这里

RMI远程服务4.png

这里他传递了一个port = 0这里传递的是一个默认值,这个部分会把远程对象发布到一个随机的端口上。然后我们继续步过到一个调用exportObject这个函数的地方,强制步入。

RMI远程服务5.png

这就是一个发布对象的静态函数。这个就是核心函数。所以上面给的注释中说如果不继承UnicastRemoteObject就需要手工导出,如果继承了这个类,就在构造函数里调用了。

然后这个静态函数呢有创建了一个UnicastServerRef类,就是服务端引用,这个就是用来处理网络请求用的。继续步入,跟到UnicastServerRef这里

RMI远程服务6.png

这里又创建了一个LiveRef类,这里开始跟视频有点不一样了,应该是JDK版本的一些差异,但大差不差,这个var1,就是port。继续步入跟进去

RMI远程服务7.png

这个var1还是port,继续步入跟进去看构造函数。

RMI远程服务8.png

var1objID,这个很清楚;var2port。然后我们看一下这个构造函数的第二个参数。

1
2
3
public static TCPEndpoint getLocalEndpoint(int var0) {
return getLocalEndpoint(var0, (RMIClientSocketFactory) null, (RMIServerSocketFactory) null);
}

我们再来看一下这个TCPEndpoint的构造函数

1
2
3
public TCPEndpoint(String var1, int var2) {
this(var1, var2, (RMIClientSocketFactory) null, (RMIServerSocketFactory) null);
}

这里的var1ip hostvar2是端口port,然后我这里的nullRMIClientSocketFactory接口,我就再看了下这个接口的源码

RMI远程服务9.png

很明显它负责服务端ServerSocket的创建。

我们继续步入进入LiveRef的构造函数,继续步过

RMI远程服务10.png

然后这里我们就可以看到我们的ip和port,然后这里的TCPTransport这个才是真正处理网络请求的东西,就是说我们的这个TCPEndpoint也是一层封装,然后这里我们需要记一下这个LiveRef,这里挡住了是601。

然后我们步过回来,一直步过回到UnicastServerRef,再步入就会到UnicastServerRef父类的构造函数,但其实这个对应的一个是服务端,一个是客户端。

RMI远程服务11.png

这里的LiveRef还是这个601。然后我们步过出来到exportObject步入进去

RMI远程服务12.png

然后这里的LiveRef还是601,我们继续步入。就到了exportObject的一个构造函数

RMI远程服务13.png

这里实际上创建了一个代理,var1implvar2datavar3permanentvar4implClassvar5stub,这个stub就是客户端真正的操作的代理,用来处理网络请求的。然后两下步入进入到createProxy

RMI远程服务14.png

这个var0implClass就是远程对象的类,var1ClientRef就是一个封装的LiveRef这里的LiveRef还是601,var2forceStubUse

然后步过两次就会进入到一个判断

1
2
3
4
5
if (var2 || !ignoreStubClasses && stubClassExists(var3)) {
return createStub(var3, var1);
} else {
...
}

实际上就是只要stub类存在就为真,执行createStub

1
2
3
4
5
6
7
8
9
10
11
private static boolean stubClassExists(Class<?> var0) {
if (!withoutStubs.containsKey(var0)) {
try {
Class.forName(var0.getName() + "_Stub", false, var0.getClassLoader());
return true;
} catch (ClassNotFoundException var2) {
withoutStubs.put(var0, (Object)null);
}
}
return false;
}

但其实有一些类是已经定义好了的

RMI远程服务15.png

如果要调用这些类的话返回值就是真。然后再步过到else这段

1
2
3
4
5
6
7
8
9
10
11
12
13
final ClassLoader var4 = var0.getClassLoader();
final Class[] var5 = getRemoteInterfaces(var0);
final RemoteObjectInvocationHandler var6 = new RemoteObjectInvocationHandler(var1);

try {
return (Remote)AccessController.doPrivileged(new PrivilegedAction<Remote>() {
public Remote run() {
return (Remote)Proxy.newProxyInstance(var4, var5, var6);
}
});
} catch (IllegalArgumentException var8) {
throw new StubNotFoundException("unable to create proxy", var8);
}

这段就是创建动态代理的一个标准的流程。

RMI远程服务16.png

这个var0implClassvar1ClientRefvar2forceStubUsevar3remoteClassvar4loader加载器AooClassLoadervar5interface远程接口,var6handler调用处理器这里面也还是封装了一个LiveRef@601

然后步过下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
Class var4 = var1.getClass();

Remote var5;
try {
var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
} catch (IllegalArgumentException var7) {
throw new ExportException("remote object implements illegal remote interface", var7);
}

if (var5 instanceof RemoteStub) {
this.setSkeleton(var1);
}

Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
this.ref.exportObject(var6);
this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
return var5;
}

var1implvar2datavar3permanentvar4implClassvar5stub,var6target

这里有一个setSkeleton,这个是跟stub对应的一个函数,如果这个impl是系统内置的就会调用这个。

然后继续步过,这里它创建了一个target这个可以理解为一个总封装,就是创建的这些有用的都会放到这里面

我这里直接ctrl查看这个Target源码(这里我按视频这里跟不进去不知道为什么),所以这段我就用视频里的了

反正就是有id,有weakimpl远程对象,disp服务端引用里面有一个LiveRef@601stub里也有LiveRef@601同时stub里的idTarget也有着同一个id。所以核心就是这个LiveRef

然后步过一下

1
this.ref.exportObject(var6);

他把这个targer发布出去了。再步入我们来看这个exportObject

RMI远程服务17.png

这里有一个TCPEndpoint@862还有一个TCPTransport@866所以这里先调用EndpointexportObject

RMI远程服务18.png

实际上也就调用了这个TCPTransport@866这个

RMI远程服务19.png

这有一个listen()这就是说它会真正的去处理网络请求

步入进去看listen()源码

RMI远程服务20.png

很明显这个this就是上面提到的TCPTransport@866

继续步过会到下面这段

1
2
3
4
5
6
7
8
9
try {
this.server = var1.newServerSocket();
Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new AcceptLoop(this.server), "TCP Accept-" + var2, true));
var3.start();
} catch (BindException var4) {
throw new ExportException("Port already in use: " + var2, var4);
} catch (IOException var5) {
throw new ExportException("Listen failed on port: " + var2, var5);
}

这里呢创建了一个新的服务端Socket等待连接,然后又创建了一个新的线程,这个线程很明显就是来处理连接之后干嘛的,然后查看一下这个AcceptLoop()的源码

AcceptLoop()里有一个run()函数,如果后续客户端连到了这个线程的话就会走进这个run的逻辑

1
2
3
4
5
6
7
8
9
10
public void run() {
try {
this.executeAcceptLoop();
} finally {
try {
this.serverSocket.close();
} catch (IOException var7) {
}
}
}

然后他的逻辑主要是在executeAcceptLoop()这里

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
private void executeAcceptLoop() {
if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) {
TCPTransport.tcpLog.log(Log.BRIEF, "listening on port " + TCPTransport.this.getEndpoint().getPort());
}

while(true) {
Socket var1 = null;

try {
var1 = this.serverSocket.accept();
InetAddress var16 = var1.getInetAddress();
String var3 = var16 != null ? var16.getHostAddress() : "0.0.0.0";

try {
TCPTransport.connectionThreadPool.execute(TCPTransport.this.new ConnectionHandler(var1, var3));
} catch (RejectedExecutionException var11) {
TCPTransport.closeSocket(var1);
TCPTransport.tcpLog.log(Log.BRIEF, "rejected connection from " + var3);
}
} catch (Throwable var15) {
Throwable var2 = var15;

try {
if (this.serverSocket.isClosed()) {
return;
}

try {
if (TCPTransport.tcpLog.isLoggable(Level.WARNING)) {
TCPTransport.tcpLog.log(Level.WARNING, "accept loop for " + this.serverSocket + " throws", var2);
}
} catch (Throwable var13) {
}
} finally {
if (var1 != null) {
TCPTransport.closeSocket(var1);
}

}

if (!(var15 instanceof SecurityException)) {
try {
TCPEndpoint.shedConnectionCaches();
} catch (Throwable var12) {
}
}

if (!(var15 instanceof Exception) && !(var15 instanceof OutOfMemoryError) && !(var15 instanceof NoClassDefFoundError)) {
if (var15 instanceof Error) {
throw (Error)var15;
}

throw new UndeclaredThrowableException(var15);
}

if (!this.continueAfterAcceptFailure(var15)) {
return;
}
}
}
}

这里都是网络请求的一些操作,就是开了一个新的线程,网络请求的线程和真正的代码逻辑的线程是独立的

这里结束之后,会产生一个默认的端口,就是这个listen()里面的。这就已经把远程对象发布出去了,但这时候发布给一个随机的端口,客户端默认是不知道的。然后我们继续步过到super.exportObject步入

1
2
3
4
public void exportObject(Target var1) throws RemoteException {
var1.setExportedTransport(this);
ObjectTable.putTarget(var1);
}

这个就是一个记录用的。继续步入一直到setExportedTransport这里再步过出去到ObjectTable.putTarget(var1);这里调用了一个静态方法

然后继续步过就可以到

1
2
objTable.put(var1, var0);
implTable.put(var2, var0);

这是两个Mapvar0就是之前说的target,它把target保存在了系统的一个静态的表里。

这就是一个服务端的一整个发布的流程。

创建注册中心

这里,就在Registry r = LocateRegistry.createRegistry(1099)这打个断点。强制步入进去

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

这就是一个创建注册中心的静态方法

然后我们进RegistryImpl()这有个if打个断点,然后步入下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (var1 == 1099 && System.getSecurityManager() != null) {
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
RegistryImpl.this.setup(new UnicastServerRef(var1x, (var0) -> {
return RegistryImpl.registryFilter(var0);
}));
return null;
}
}, (AccessControlContext)null, new SocketPermission("localhost:" + var1, "listen,accept"));
} catch (PrivilegedActionException var3) {
throw (RemoteException)var3.getException();
}
} else {
LiveRef var2 = new LiveRef(id, var1);
this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));
}

就是先对var1也就是端口,进行判断是否等于1099,然后检查系统的安全管理是否不为空。

然后再步过下来,就直接到下面这段else然后创建了一个新的LiveRef和一个新的UnicastServerRef,这里的话跟上面服务端的创建方式是差不多的。

我们来看一下他这里的var2,就是这个lref,这里的port也就是前面设置的1099

RMI注册绑定1.png

然后可以步入去看一下这个UnicastServerRef的源码,这里跟上面的这个服务端使用的UnicastServerRef是一样的,就是这个setup有一点点不一样,我们可以步入去看

1
2
3
4
private void setup(UnicastServerRef var1) throws RemoteException {
this.ref = var1;
var1.exportObject(this, (Object)null, true);
}

然后继续步入到exportObject这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
Class var4 = var1.getClass();

Remote var5;
try {
var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
} catch (IllegalArgumentException var7) {
throw new ExportException("remote object implements illegal remote interface", var7);
}

if (var5 instanceof RemoteStub) {
this.setSkeleton(var1);
}

Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
this.ref.exportObject(var6);
this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
return var5;
}

这里的var3permanent值为true说明这个创建的对象是永久的,这个var5stub,这里就是创建了一个stub然后继续步入去看createProxy()

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
public static Remote createProxy(Class<?> var0, RemoteRef var1, boolean var2) throws StubNotFoundException {
Class var3;
try {
var3 = getRemoteClass(var0);
} catch (ClassNotFoundException var9) {
throw new StubNotFoundException("object does not implement a remote interface: " + var0.getName());
}

if (var2 || !ignoreStubClasses && stubClassExists(var3)) {
return createStub(var3, var1);
} else {
final ClassLoader var4 = var0.getClassLoader();
final Class[] var5 = getRemoteInterfaces(var0);
final RemoteObjectInvocationHandler var6 = new RemoteObjectInvocationHandler(var1);

try {
return (Remote)AccessController.doPrivileged(new PrivilegedAction<Remote>() {
public Remote run() {
return (Remote)Proxy.newProxyInstance(var4, var5, var6);
}
});
} catch (IllegalArgumentException var8) {
throw new StubNotFoundException("unable to create proxy", var8);
}
}
}

继续步入去看stubClassExists()

1
2
3
4
5
6
7
8
9
10
11
12
private static boolean stubClassExists(Class<?> var0) {
if (!withoutStubs.containsKey(var0)) {
try {
Class.forName(var0.getName() + "_Stub", false, var0.getClassLoader());
return true;
} catch (ClassNotFoundException var2) {
withoutStubs.put(var0, (Object)null);
}
}

return false;
}

首先它会判断有没有系统中_Stub后缀的,然后这个是有的,有一个RegistryImpl_Stub.class这个是JDK自带的类。然后用forName对这个类进行初始化,然后这里跟视频里的那个是不太一样的,JDK版本的差异,我的版本这直接给他返回了一个true,但实际的效果是差不多的。

然后再步过到exportObject这里,我们步过下来看stub,也就是这里的var5

RMI注册绑定2.png

我们这创建的注册中心的stub就是这个RegistryImpl_Stub,而我们当时创建的服务端的stub是一个动态代理,就是靠这个动态代理的调用处理器来找到文件。

光标指着的这里,就是说如果var5也就是stub如果是服务端定义好的,就会调用下面的setSkeleton方法,我们直接点进去看这个setSkeleton,然后下个断点,步入进去。

1
2
3
4
5
6
7
8
9
public void setSkeleton(Remote var1) throws RemoteException {
if (!withoutSkeletons.containsKey(var1.getClass())) {
try {
this.skel = Util.createSkeleton(var1);
} catch (SkeletonNotFoundException var3) {
withoutSkeletons.put(var1.getClass(), (Object)null);
}
}
}

这里的有一个判断,就是判断这个impl也就是这里var1是否有SkeletonSkeleton就是服务端的处理网络请求的代理。然后步过就会到下面的try这段语句,创建一个Skeleton,步入去看怎么创建的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static Skeleton createSkeleton(Remote var0) throws SkeletonNotFoundException {
Class var1;
try {
var1 = getRemoteClass(var0.getClass());
} catch (ClassNotFoundException var8) {
throw new SkeletonNotFoundException("object does not implement a remote interface: " + var0.getClass().getName());
}

String var2 = var1.getName() + "_Skel";

try {
Class var3 = Class.forName(var2, false, var1.getClassLoader());
return (Skeleton)var3.newInstance();
} catch (ClassNotFoundException var4) {
throw new SkeletonNotFoundException("Skeleton class not found: " + var2, var4);
} catch (InstantiationException var5) {
throw new SkeletonNotFoundException("Can't create skeleton: " + var2, var5);
} catch (IllegalAccessException var6) {
throw new SkeletonNotFoundException("No public constructor: " + var2, var6);
} catch (ClassCastException var7) {
throw new SkeletonNotFoundException("Skeleton not of correct class: " + var2, var7);
}
}

很明显,这个创建方式,跟前面的那个stub差不多,也是直接一个forName创建。

然后步过到return,然后出来,就有了这个skel,这是UnicastServerRef的一个内部变量。它在这个impl也就是这个var1,在它的ref里。

RMI注册绑定3.png

然后下一步就是创建一个target把这些都放进去。

接下来步过到下面这个exportObject这里,我们步入进去到synchronized(this)这里再过下来到super.exportObject(var1),然后我们步入进去看

1
2
3
4
public void exportObject(Target var1) throws RemoteException {
var1.setExportedTransport(this);
ObjectTable.putTarget(var1);
}

target放进去的时候会调用一个静态方法,我们到pubTarget这里步入去看

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
static void putTarget(Target var0) throws ExportException {
ObjectEndpoint var1 = var0.getObjectEndpoint();
WeakRef var2 = var0.getWeakImpl();
if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + var1);
}

synchronized(tableLock) {
if (var0.getImpl() != null) {
if (objTable.containsKey(var1)) {
throw new ExportException("internal error: ObjID already in use");
}

if (implTable.containsKey(var2)) {
throw new ExportException("object already exported");
}

objTable.put(var1, var0);
implTable.put(var2, var0);
if (!var0.isPermanent()) {
incrementKeepAliveCount();
}
}
}
}

我们直接步过到put这里看

RMI注册绑定4.png

所有创建好的远程对象都放在这里,我们已经创建好了一个叫RemoteObjImpl的远程对象然后又创建了一个RegistryImpl,但是这里有三个。然后我们来看下value

RMI注册绑定5.png

然后可以看到这有一个DGCImpl_Stub,这个不是我们所创建的,DGC是分布式垃圾回收,是一个默认会创建的对象。另外两个obj其中一个的stub$Proxy就是最开始创建远程服务的stub里面放了个LiveRef,它还有一个disp这是UnicastServerRef里面也放了个LiveRef,这两个是不一样的,但是他们的端口是一样的;另外一个的stubRegistryImpl_Stub,这个objdisp里有一个skelRegistryImpl_Skel,然后stubskel里都有个LiveRef这两个是一样的。

绑定

r.bind("remoteObj", remoteObj);这里打个断点,强制步入进去

1
2
3
4
5
6
7
8
9
10
public void bind(String var1, Remote var2) throws RemoteException, AlreadyBoundException, AccessException {
synchronized(this.bindings) {
Remote var4 = (Remote)this.bindings.get(var1);
if (var4 != null) {
throw new AlreadyBoundException(var1);
} else {
this.bindings.put(var1, var2);
}
}
}

组长这JDK的bind比我这多了一段checkAccess("Registry.bind"),这个就是在本地绑定,都是能通过的。可能是因为这一步没什么必要,所以稍微新一点的JDK没有这一段。

然后这个bindings就是一个HashTable,如果里面有叫这个var1的也就是我上面设置的remoteObject,就会抛出一个已经绑定的异常,没有就把它put进去。

RMI的实现上要求服务端和注册中心在一台机子上,但是在低版本JDK中有一些实现上的问题所以允许远程绑定的

客户端

下面是客户端需要的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// RMIClient.java
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
remoteObj.sayHello("hello");
}
}

// IRemoteObj.java
import java.rmi.Remote;
import java.rmi.RemoteException;

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

请求注册中心(客户端)

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);打个断点,开始debug,强制步入,再步入。

然后步过下来,主要执行的就是下面这段代码

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

这里只是进行了一个本地创建,把IP和端口传进来之后就创建了一个LiveRef把IP和端口放进去进行了封装,然后又调了createProxy,进去看createProxy之后把断点放在var3 = getRemoteClass(var0);

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
public static Remote createProxy(Class<?> var0, RemoteRef var1, boolean var2) throws StubNotFoundException {
Class var3;
try {
var3 = getRemoteClass(var0);
} catch (ClassNotFoundException var9) {
throw new StubNotFoundException("object does not implement a remote interface: " + var0.getName());
}

if (var2 || !ignoreStubClasses && stubClassExists(var3)) {
return createStub(var3, var1);
} else {
final ClassLoader var4 = var0.getClassLoader();
final Class[] var5 = getRemoteInterfaces(var0);
final RemoteObjectInvocationHandler var6 = new RemoteObjectInvocationHandler(var1);

try {
return (Remote)AccessController.doPrivileged(new PrivilegedAction<Remote>() {
public Remote run() {
return (Remote)Proxy.newProxyInstance(var4, var5, var6);
}
});
} catch (IllegalArgumentException var8) {
throw new StubNotFoundException("unable to create proxy", var8);
}
}
}

然后这个流程就跟服务端创建RegistryImpl_Stub的流程是一摸一样的,服务端创建的stub并没有通过序列化反序列化传过来,而是把参数传过来,然后在本地又创建了一个

RMI请求注册1.png

然后这就有了一个RegistryImol_Stub,然后这里就获取到了注册中心的stub对象,然后下一步就是通过这个来获取远程对象,但实际上也是获取这个动态代理stub

然后我们去看lookup的逻辑,点进去打断点步过

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 Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
StreamRemoteCall var2 = (StreamRemoteCall)this.ref.newCall(this, operations, 2, 4905912898345647071L);

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

this.ref.invoke(var2);

Remote var20;
try {
ObjectInput var4 = var2.getInputStream();
var20 = (Remote)var4.readObject();
} catch (IOException | ClassNotFoundException | ClassCastException var13) {
var2.discardPendingRefs();
throw new UnmarshalException("error unmarshalling return", var13);
} finally {
this.ref.done(var2);
}

return var20;
} catch (RuntimeException var16) {
throw var16;
} catch (RemoteException var17) {
throw var17;
} catch (NotBoundException var18) {
throw var18;
} catch (Exception var19) {
throw new UnexpectedException("undeclared checked exception", var19);
}
}

我们把这个名称也就是我们这里的remoteObj传进去然后穿进去之后,把它写进了一个输出流里面,也就是序列化进去了,然后这里既然有序列化那么注册中心那里就需要反序列化进行读取,那么就说明注册中心那里有一个反序列化点。

然后它会拿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;
}
}

它会调用一个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
switch (var1) {
case 1:
return;
case 2:
Object var14;
try {
var14 = this.in.readObject();
} catch (Exception var10) {
this.discardPendingRefs();
throw new UnmarshalException("Error unmarshaling return", var10);
}

if (!(var14 instanceof Exception)) {
this.discardPendingRefs();
throw new UnmarshalException("Return type not Exception");
} else {
this.exceptionReceivedFromServer((Exception)var14);
}
default:
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF, "return code invalid: " + var1);
}

throw new UnmarshalException("Return code invalid");
}

这里有一个2号异常会通过反序列化来获取流里的对象,设计的本意可能是通过反序列化来获取这一个异常类的更详细的信息,但如果注册中心返回一个恶意的流,那么客户端就会在这里进行反序列化,也会导致客户端被攻击,然后这个反序列化点更隐蔽影响范围也更广,因为所有的stub里处理网络请求的都会调用这个方法

然后我们返回去看lookup它完成了这个invoke之后,它又会去获取一个输入流,然后通过反序列化的方式读出来

1
2
ObjectInput var4 = var2.getInputStream();
var20 = (Remote)var4.readObject();

就是这个var20就是读回来的动态代理,就是那个远程对象的动态代理。既然这是反序列化读出来的,那么如果有一个恶意的注册中心就可以通过这个方式来攻击客户端

请求服务端(客户端)

remoteObj.sayHello("hello");这里打个断点进去,强制步入然后会跳到invoke

因为我们当前获取的这个remoteObj是一个动态代理,动态代理不管调用什么方法都会走到调用处理器里面的invoke方法。

然后步过下来到return invokeRemoteMethod(proxy, method, args);,这里调用了一个UnicastRef.invoke,但是这是一个重载的方法,步入看一下这个invoke的逻辑。

这里还是创建了一个连接,比较类似。

步过下来,有一个marshalValue((Class)((Object[])var11)[var12], var3[var12], var10);,这个marshalValue会先序列化一个值,序列化的就是我们传的这个参数”hello”

RMI请求服务1.png

可以来看一下这个函数的逻辑

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 void marshalValue(Class<?> var0, Object var1, ObjectOutput var2) throws IOException {
if (var0.isPrimitive()) {
if (var0 == Integer.TYPE) {
var2.writeInt((Integer)var1);
} else if (var0 == Boolean.TYPE) {
var2.writeBoolean((Boolean)var1);
} else if (var0 == Byte.TYPE) {
var2.writeByte((Byte)var1);
} else if (var0 == Character.TYPE) {
var2.writeChar((Character)var1);
} else if (var0 == Short.TYPE) {
var2.writeShort((Short)var1);
} else if (var0 == Long.TYPE) {
var2.writeLong((Long)var1);
} else if (var0 == Float.TYPE) {
var2.writeFloat((Float)var1);
} else {
if (var0 != Double.TYPE) {
throw new Error("Unrecognized primitive type: " + var0);
}

var2.writeDouble((Double)var1);
}
} else {
var2.writeObject(var1);
}
}

首先判断是不是基础类型,如果不是就writeObject进去,然后步过下来,它又调用了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
protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
if (var0.isPrimitive()) {
if (var0 == Integer.TYPE) {
return var1.readInt();
} else if (var0 == Boolean.TYPE) {
return var1.readBoolean();
} else if (var0 == Byte.TYPE) {
return var1.readByte();
} else if (var0 == Character.TYPE) {
return var1.readChar();
} else if (var0 == Short.TYPE) {
return var1.readShort();
} else if (var0 == Long.TYPE) {
return var1.readLong();
} else if (var0 == Float.TYPE) {
return var1.readFloat();
} else if (var0 == Double.TYPE) {
return var1.readDouble();
} else {
throw new Error("Unrecognized primitive type: " + var0);
}
} else {
return var0 == String.class && var1 instanceof ObjectInputStream ? SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)var1) : var1.readObject();
}
}

我们可以看到最后怎么去获取结果就是用反序列化来进行获取,这里虽然跟组长的不太一样,我这里做了一些限制。就是说如果var0不是字符类型或者var1不是ObjectInputStream的实例,那么就会调用var1.readObject

这个就是客户端的另一个反序列化点。

这里客户端的流程就完了

然后我们之前说的那个executeCall()这个点,所处理的协议就是JRMP协议。基本上通过JRMP的攻击的就是这个stub

客户端请求注册中心(注册中心)

上面有提到服务端操作的是这个skeleton,所以我们需要把断点下到_skelRegistryImpl_Skel里来查看流程,我们先进入到Registry r = LocateRegistry.createRegistry(1099);createRegistry()方法里,然后进入到RegistryImpl,然后到下面的this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));这个setup()方法里,再进入到exportObject()方法里,然后进入this.ref.exportObject(var6);exportObject()方法里。继续进入exportObject()方法,接着进入,继续到this.transport.exportObject(var1);exportObject()方法,然后这里有个listen()方法,他开了个网络监听出去,我们从这里进去,到下面有一个Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new AcceptLoop(this.server), "TCP Accept-" + var2, true));我们去看这个新的线程AcceptLoop(),然后去看这个线程里的run()方法,它调用了一个this.executeAcceptLoop(),我们继续进入去看。

它调用了一个TCPTransport.connectionThreadPool.execute(TCPTransport.this.new ConnectionHandler(var1, var3));,我们继续进去看这个ConnectionHandler()方法,然后下来去看它的run()方法,它实际上就是调用了一个this.run0()我们接着去看这个run0()方法,它会去协议里传过来的一些字段还有写POST的,但是最主要的是他会调用一个TCPTransport.this.handleMessages(var14, true);我们继续进入到这个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
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
void handleMessages(Connection var1, boolean var2) {
int var3 = this.getEndpoint().getPort();

try {
DataInputStream var4 = new DataInputStream(var1.getInputStream());

do {
int var5 = var4.read();
if (var5 == -1) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + var3 + ") connection closed");
}

return;
}

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

switch (var5) {
case 80:
StreamRemoteCall var6 = new StreamRemoteCall(var1);
if (!this.serviceCall(var6)) {
return;
}
break;
case 81:
case 83:
default:
throw new IOException("unknown transport op " + var5);
case 82:
DataOutputStream var7 = new DataOutputStream(var1.getOutputStream());
var7.writeByte(83);
var1.releaseOutputStream();
break;
case 84:
DGCAckHandler.received(UID.read(var4));
}
} while(var2);

} catch (IOException var17) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + var3 + ") exception: ", var17);
}

} finally {
try {
var1.close();
} catch (IOException var16) {
}
}
}

默认的casecase 80然后它调用一个this.serviceCall(var6),进去看这个serviceCall(),有一段Target var5 = ObjectTable.getTarget(new ObjectEndpoint(var39, var40));,就是从静态表里去获取一个Target,然后我们把断点下这就可以了。开始调试!

不知道为什么怎么调都调不进去,悲

那就直接用组长的吧

RMI请求注册(注册)1.png

这个target里有一个stub其实就是服务端的RegistryImpl_stub,然后他会final Dispatcher var6 = var5.getDispatcher();来获取一个Dispatcher就是一个分发器,然后获取到的分发器就是这个UnicastServerRef

RMI请求注册(注册)2.png

后面他会调用dispdispatch方法,步入进去然后这个dispatch又会调用一个oldDispatch()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void oldDispatch(Remote var1, RemoteCall var2, int var3) throws Exception {
ObjectInput var6 = var2.getInputStream();

try {
Class var7 = Class.forName("sun.rmi.transport.DGCImpl_Skel");
if (var7.isAssignableFrom(this.skel.getClass())) {
((MarshalInputStream)var6).useCodebaseOnly();
}
} catch (ClassNotFoundException var9) {
}

long var4;
try {
var4 = var6.readLong();
} catch (Exception var8) {
throw new UnmarshalException("error unmarshalling call header", var8);
}

Operation[] var10 = this.skel.getOperations();
this.logCall(var1, var3 >= 0 && var3 < var10.length ? var10[var3] : "op: " + var3);
this.unmarshalCustomCallData(var6);
this.skel.dispatch(var1, var2, var3, var4);
}

在最后他又会调用一个skel.dispatch()方法,这里就是调用RegistryImpl_Skeldispatch()了。

客户端的时候我们处理了这个stub,到服务端我们就会去处理这个skeleton,注册中心实际上就是一个特殊的服务端。

这个dispatch()下面有一段case语句,不同的case对应着不同的方法,case 0就是bind方法,case 1就是list()方法,case 2lookup()方法

然后我们可以看到在注册中心的Skel里直接使用了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
case 0:
RegistryImpl.checkAccess("Registry.bind");

try {
var10 = (ObjectInputStream)var7.getInputStream();
var8 = SharedSecrets.getJavaObjectInputStreamReadString().readString(var10);
var81 = (Remote)var10.readObject(); // 反序列化读取
} catch (IOException | ClassNotFoundException | ClassCastException var78) {
var7.discardPendingRefs();
throw new UnmarshalException("error unmarshalling arguments", var78);
} finally {
var7.releaseInputStream();
}

var6.bind(var8, var81);

try {
var7.getResultStream(true);
break;
} catch (IOException var77) {
throw new MarshalException("error marshalling return", var77);
}
case 1:
...
case 2:
...
case 3:
RegistryImpl.checkAccess("Registry.rebind");

try {
var10 = (ObjectInputStream)var7.getInputStream();
var8 = SharedSecrets.getJavaObjectInputStreamReadString().readString(var10);
var81 = (Remote)var10.readObject(); // 反序列化读取
} catch (IOException | ClassNotFoundException | ClassCastException var71) {
var7.discardPendingRefs();
throw new UnmarshalException("error unmarshalling arguments", var71);
} finally {
var7.releaseInputStream();
}

var6.rebind(var8, var81);

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

前面有说过,我们这个远程对象的名称是通过序列化传过去的,那么注册中心这六是通过反序列化读取出来的。这里就会有一个反序列化点,我们的客户端就可以通过这来攻击注册中心,只要他有readObject()方法。

客户端请求服务端(服务端)

这一块前面这跟网络相关的部分都是一样的也会到disp.dispatch()这里

RMI请求注册(注册)3.png

这里的skelDGCImpl_Skel就暂时先不看。继续下来

RMI请求注册(注册)4.png

当前的target是这个动态代理,然后再下来也会调用这个dispatch()这时的dispUnicastServerRef,然后再步入进去到UnicastServerRefdispatch,到这里都是跟前面一样的。

不一样的是这里的skel是空的,所以就会运行到下面的代码

1
2
3
MarshalInputStream var7 = (MarshalInputStream)var41; // 获取输入流
var7.skipDefaultResolveClass();
Method var42 = (Method)this.hashToMethod_Map.get(var4); // 获取method,就是我们定义的那个远程方法

但是最重要的是下面这段代码

1
2
this.unmarshalCustomCallData(var41);
var9 = this.unmarshalParameters(var1, var42, var7);

它会把传过来的参数值反序列化出来,我们步入去看unmarshalParameters()方法,然后他会调用unmarshalParametersChecked()unmarshalParametersUnchecked()方法,然后这两个方法都会调用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
protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
if (var0.isPrimitive()) {
if (var0 == Integer.TYPE) {
return var1.readInt();
} else if (var0 == Boolean.TYPE) {
return var1.readBoolean();
} else if (var0 == Byte.TYPE) {
return var1.readByte();
} else if (var0 == Character.TYPE) {
return var1.readChar();
} else if (var0 == Short.TYPE) {
return var1.readShort();
} else if (var0 == Long.TYPE) {
return var1.readLong();
} else if (var0 == Float.TYPE) {
return var1.readFloat();
} else if (var0 == Double.TYPE) {
return var1.readDouble();
} else {
throw new Error("Unrecognized primitive type: " + var0);
}
} else {
return var0 == String.class && var1 instanceof ObjectInputStream ? SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)var1) : var1.readObject();
}
}

这个就是对传入的值进行一个类型的检查然后反序列化出来。

然后步过出来之后,上面的这个var9也就是这个params就是我们传过来的这个字符串,就是这个hello

然后下来到这里

1
2
3
4
5
6
Object var10;
try {
var10 = var42.invoke(var1, var9);
} catch (InvocationTargetException var33) {
throw var33.getTargetException();
}

这里是真正的去进行远程调用。然后就是这个返回值会把它进行序列化,然后到客户端再进行反序列化。这是一个对称的过程。所以客户端和服务端可以互相打

1
2
3
4
5
6
try {
ObjectOutput var11 = var2.getResultStream(true);
Class var12 = var42.getReturnType();
if (var12 != Void.TYPE) {
marshalValue(var12, var10, var11);
}

客户端请求服务端(dgc)

DGCRMI的一个分布式垃圾回收的模块。那么它在哪呢

就是在创建完远程对象的时候会把他封装进一个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
static void putTarget(Target var0) throws ExportException {
ObjectEndpoint var1 = var0.getObjectEndpoint();
WeakRef var2 = var0.getWeakImpl();
if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) { // 在这打个断点
DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + var1);
}

synchronized(tableLock) {
if (var0.getImpl() != null) {
if (objTable.containsKey(var1)) {
throw new ExportException("internal error: ObjID already in use");
}

if (implTable.containsKey(var2)) {
throw new ExportException("object already exported");
}

objTable.put(var1, var0); // 这也打一个
implTable.put(var2, var0);
if (!var0.isPermanent()) {
incrementKeepAliveCount();
}
}
}
}

步过下来

RMI请求注册(dgc)1.png

这里这个targetstub存的就是这个DGCImpl_Stub然后我们继续步过,看看它put进去之后会发生什么

RMI请求注册(dgc)2.png

这时我们这里的stub变成了那个动态代理,但实际上我们把这个动态代理放进去的时候这个静态表里就有了一个target

RMI请求注册(dgc)3.png

这主要就是因为下面这段代码

1
2
3
if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + var1);
}

这个DGCImpl.dgcLog是一个静态变量,当我们对这个类的静态变量进行调用的时候,会完成这个类的初始化

然后我们看DGCImpl的源码一直下来到static{}这段

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
static {
leaseCheckInterval = (Long)AccessController.doPrivileged(new GetLongAction("sun.rmi.dgc.checkInterval", leaseValue / 2L));
scheduler = ((RuntimeUtil)AccessController.doPrivileged(new RuntimeUtil.GetInstanceAction())).getScheduler();
DGC_MAX_DEPTH = 5;
DGC_MAX_ARRAY_SIZE = 10000;
dgcFilter = (ObjectInputFilter)AccessController.doPrivileged(DGCImpl::initDgcFilter);
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ClassLoader var1 = Thread.currentThread().getContextClassLoader();

try {
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());

try {
DGCImpl.dgc = new DGCImpl();
final ObjID var2 = new ObjID(2);
LiveRef var3 = new LiveRef(var2, 0);
final UnicastServerRef var4 = new UnicastServerRef(var3, (var0) -> {
return DGCImpl.checkInput(var0);
});
final Remote var5 = Util.createProxy(DGCImpl.class, new UnicastRef(var3), true);
var4.setSkeleton(DGCImpl.dgc);
Permissions var6 = new Permissions(); // 在这打个断点
var6.add(new SocketPermission("*", "accept,resolve"));
ProtectionDomain[] var7 = new ProtectionDomain[]{new ProtectionDomain((CodeSource)null, var6)};
AccessControlContext var8 = new AccessControlContext(var7);
Target var9 = (Target)AccessController.doPrivileged(new PrivilegedAction<Target>() {
public Target run() {
return new Target(DGCImpl.dgc, var4, var5, var2, true);
}
}, var8);
ObjectTable.putTarget(var9);
} catch (RemoteException var13) {
throw new Error("exception initializing server-side DGC", var13);
}
} finally {
Thread.currentThread().setContextClassLoader(var1);
}

return null;
}
});
}

然后我们步过,就到了DGCImpl.dgc = new DGCImpl();这段,然后再看下面这段

1
2
3
4
5
final UnicastServerRef var4 = new UnicastServerRef(var3, (var0) -> {
return DGCImpl.checkInput(var0);
});
final Remote var5 = Util.createProxy(DGCImpl.class, new UnicastRef(var3), true); // 在这打个断点
var4.setSkeleton(DGCImpl.dgc);

然后这段就和前面创建代理的时候很像了,然后步过就会到上面打断点的这行,然后我们步入进去,然后就会步过到下面这段

1
2
3
if (var2 || !ignoreStubClasses && stubClassExists(var3)) {
return createStub(var3, var1); // 到这 步入进去
}

首先它会判断系统里有没有这个DGCImpl_Stub,然后我们下面可以看到是有这个类的

RMI请求注册(dgc)4.png

然后他就会实例化一个系统的DGCImpl_Stub。接下来我们步过出来回到上面的这段static{}

RMI请求注册(dgc)5.png

这里就创建了一个DGCImpl_Stub类,他的功能和注册中心是一样的也是有一个端口,只不过注册中心的那个端口用来注册服务,这个端口用来远程回收服务就是这个端口不是固定的。

这一部分创建完之后就会把它放到一个target

以上就是DGC的创建过程

然后他调用的时候就会走到这个disp

RMI请求注册(dgc)6.png

然后就是跟注册中心一样的流程,接下来看一下功能

先来看这个DGCImpl_Stub(客户端),他主要就是两个方法,一个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
64
65
66
67
public void clean(ObjID[] var1, long var2, VMID var4, boolean var5) throws RemoteException {
try {
StreamRemoteCall var6 = (StreamRemoteCall)this.ref.newCall(this, operations, 0, -669196253586618813L);
var6.setObjectInputFilter(DGCImpl_Stub::leaseFilter);

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

this.ref.invoke(var6); // jrmp攻击
this.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 {
StreamRemoteCall var5 = (StreamRemoteCall)this.ref.newCall(this, operations, 1, -669196253586618813L);
var5.setObjectInputFilter(DGCImpl_Stub::leaseFilter);

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

this.ref.invoke(var5); // jrmp攻击
Connection var7 = var5.getConnection();

Lease var22;
try {
ObjectInput var8 = var5.getInputStream();
var22 = (Lease)var8.readObject(); // 反序列化
} catch (IOException | ClassNotFoundException | ClassCastException var17) {
if (var7 instanceof TCPConnection) {
((TCPConnection)var7).getChannel().free(var7, false);
}

var5.discardPendingRefs();
throw new UnmarshalException("error unmarshalling return", var17);
} finally {
this.ref.done(var5);
}

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

然后我们看一下DGCImpl_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
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 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;
StreamRemoteCall var7 = (StreamRemoteCall)var2;
ObjID[] var8;
long var9;
switch (var3) {
case 0:
VMID var34;
boolean var36;
try {
ObjectInput var37 = var7.getInputStream();
var8 = (ObjID[])((ObjID[])var37.readObject());
var9 = var37.readLong();
var34 = (VMID)var37.readObject(); // 反序列化
var36 = var37.readBoolean();
} catch (IOException | ClassNotFoundException | ClassCastException var32) {
var7.discardPendingRefs();
throw new UnmarshalException("error unmarshalling arguments", var32);
} finally {
var7.releaseInputStream();
}

var6.clean(var8, var9, var34, var36);

try {
var7.getResultStream(true);
break;
} catch (IOException var31) {
throw new MarshalException("error marshalling return", var31);
}
case 1:
Lease var11;
try {
ObjectInput var12 = var7.getInputStream();
var8 = (ObjID[])((ObjID[])var12.readObject());
var9 = var12.readLong();
var11 = (Lease)var12.readObject(); // 反序列化
} catch (IOException | ClassNotFoundException | ClassCastException var29) {
var7.discardPendingRefs();
throw new UnmarshalException("error unmarshalling arguments", var29);
} finally {
var7.releaseInputStream();
}

Lease var35 = var6.dirty(var8, var9, var11);

try {
ObjectOutput var13 = var7.getResultStream(true);
var13.writeObject(var35);
break;
} catch (IOException var28) {
throw new MarshalException("error marshalling return", var28);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}

所以这个DGC和注册中心一样都是会被攻击的,特点就是他是一个自动生成的,只要创建了远程对象就会有这个DGC服务

我们如果去攻击这个远程对象那么我们就需要知道这个参数类型的,但是攻击DGC就不需要

JDK高版本绕过

组长这里的版本是8u65,然后我这的版本是8u124,但还是会有一定的利用点存在

比较重要的就是这个UnicastRef,这个UnicastRef里有一个invoke(),这个有一个JRMP Client的反序列化攻击,所有RMI的客户端都会受到反序列化攻击,8u121版本修复了之后,它的客户端实际上是没有修复的,只修复了针对服务端的攻击。然后从我这8u124的版本的源码来看,客户端的代码是有一些改变但是也并没有完全修(如修?),

如果能用服务端来发起一个客户端请求,就可以在服务端上导致一个反序列化攻击。那么就要找客户端请求是怎么发送的,能不能手动的弄出来一次。

就是要想怎么去调用invoke(),调用这个的就是在这两个stub里,一个是DGCImpl_Stub一个是RegistryImpl_Stub还有一个就是这个动态代理,动态代理只有我们在绑定创建服务端的时候生成。

想要调用invoke()就先来弄一个stub,想要弄一个stub就要调用createProxy

DGCstub有两个地方调用,第一个是在静态代码块(没办法干涉),第二个是DGCClient里的一个内部类EndpointEntry的构造函数里创建了一个DGC服务。因为这一整个流程是固定的,在JDK里已经写死了,如果我们想改变它的流程,就需要一个反序列化的点能够触发创建stub然后调用invoke()的流程。

在一个已经跑起来的程序中,我们想改变它的逻辑实际上是比较难的,这种动态的特性基本上只能靠反序列化来实现。所以就可以以EndpointEntry这个类作为入口,想办法创建一个这个类,然后生成一个DGC,下一步让这个DGC发起客户端请求。

我们要找到一个反序列化点,要在一个反序列化点里创建一个DGC服务。然后我们就找这个EndpointEntry的流程是什么

先是调用了一个lookup()

1
2
3
public static EndpointEntry lookup(Endpoint var0) {
...
}

继续往上找,我们要找到一个反序列化点,然后再这个反序列化点李创建一个DGC服务

然后到这里

1
2
3
4
5
6
static void registerRefs(Endpoint var0, List<LiveRef> var1) {
EndpointEntry var2;
do {
var2 = DGCClient.EndpointEntry.lookup(var0);
} while(!var2.registerRefs(var1));
}

接着往上找就有分叉路,有一个是到ConnectionInputStream,这里调用这个registerRefs()函数的时候会创建。

1
2
3
4
5
6
7
8
9
10
void registerRefs() throws IOException {
if (!this.incomingRefTable.isEmpty()) {
Iterator var1 = this.incomingRefTable.entrySet().iterator();

while(var1.hasNext()) {
Map.Entry var2 = (Map.Entry)var1.next();
DGCClient.registerRefs((Endpoint)var2.getKey(), (List)var2.getValue());
}
}
}

另一个是LiveRefread()方法的一段else里会调用

1
2
3
4
5
6
7
8
9
if (var0 instanceof ConnectionInputStream) { // 如果输入流不是ConnectionInputStream 会调用 else,但是一整个反序列化流程里输入流都是这个,所以这里行不通
ConnectionInputStream var6 = (ConnectionInputStream)var0;
var6.saveRef(var5);
if (var4) {
var6.setAckNeeded();
}
} else {
DGCClient.registerRefs(var2, Arrays.asList(var5));
}

然后我们继续网上找,看上面这个registerRefs()什么时候被调用,然后到了StreamRemoteCallreleaseInputStream这里,然后这个StreamRemoteCsll就是一个jrmp的攻击点,我们继续去看这个releaseInputCall在哪调的。它在各个skel里调用,到这里流程就通了,我们可以在服务端找到一个地方,能够让他创建一个dgc服务,然后下一步我们只要让这个dgc服务发起一个请求就可以了。

但是我们可以看到在这个ConnectionInputStreamregisterRefs()里有一个判断(源码在上面),接下来我们就需要让他走进这个判断里,也就是让他的incomingRefTable不为空就可以执之后的流程,但是正常调试流程的话他就是空的,所以就是想一下怎么让他不是空

让他不为空,就要找给他赋值的地方然后只有一个地方用了put

1
2
3
4
5
6
7
8
9
10
void saveRef(LiveRef var1) {
Endpoint var2 = var1.getEndpoint();
Object var3 = (List)this.incomingRefTable.get(var2);
if (var3 == null) {
var3 = new ArrayList();
this.incomingRefTable.put(var2, var3);
}

((List)var3).add(var1);
}

所以首先要调用这个方法,然后在上面提到的这个LiveRefread()方法里调用了,刚才也说进不去底下的else但是能进上面的这个if,然后他就会调用这个saveRef,然后我们就需要知道这个read()函数在哪里被调用,继续上去找,就会发现在UnicastRefreadExternal()这个是JAVA原生反序列化的另一种,和readObject类似但不完全一样,就是反序列化的时候如果有readExternal()他也会调用。

那么就已经完成了想要创建dgc的这个逻辑。

我们首先序列化一个UnicastRef对象,然后在里面保存一个这个ref,我们把它传过去可以正常反序列化,然后在他的反序列化流程里,他会调用这个read()方法,然后这个方法就会调用这个savaRef,这个saveRef就会把这个incomingRefTable赋值,就不会为空了。下一步就到skel里,接下来就正常走反序列化的流程,到他的这个releaseInputStream()的时候就会调用registerRefs(),此时就是上面说的这个判断,不为空了。所以他的反序列化只是用来给这个incomingRefTable赋值,然后让他走到正常的调用流程,真正的触发攻击是在正常的调用流程里面。

但是到后面也只是创建了一个dgc对象但不能发起请求

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()他是调用了自己里面的一个线程,我们点进去看。最后面有一段

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); // 调用了makeDirtyCall方法
}

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

return null;
}
}, DGCClient.SOCKET_ACC);

然后我们去看这个makeDirtyCall方法,其中有一段

1
2
3
4
5
6
7
8
9
10
11
try {
Lease var20 = this.dgc.dirty(var4, var2, new Lease(DGCClient.vmid, DGCClient.leaseValue)); // 这里调了dgc.dirty
var8 = var20.getValue();
long var10 = DGCClient.computeRenewTime(var5, var8);
var12 = var5 + var8;
synchronized(this) {
this.dirtyFailures = 0;
this.setRenewTime(var10);
this.expirationTime = var12;
}
}

也就是说,在利用的后半段,满足那个if条件之后,从创建dgc到调用dgcdirty方法来触发客户端请求,导致了jrmp的反序列化,后半段的流程完全就是JDK里写好了的流程,相当于就是这么设计的。

就是说在这个skel里,在他释放一个InputStream的时候,他就会有一个创建dgc的过程,但正常来说默认是走不进去的,主要就是因为有一个if判断,然后我们想要改变这个if判断我们做的事就是传了一个UnicastRef,然后在他反序列化的时候让他走进了这个流程。

这么做的好处就是我们在服务端发起一个客户端的反序列化请求,然后我们之前有说服务端的请求是会被过滤的,但注册中心想要反序列化的时候,正常来说服务端反序列化是会被过滤的,但是客户端反序列化就是jrmp的那个是不会被过滤的,就是说我们还是可以绕过这些限制进行攻击。

总结

具体的攻击手法在ysoserial里基本都写好了,来看下这个exploit

RMI总结1.png

这个RMIRegistryExploit就是直接攻击注册中心,就是我们前面说直接攻击注册中心的,这种攻击在8u121之后就修复了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void exploit(final Registry registry,
final Class<? extends ObjectPayload> payloadClass,
final String command) throws Exception {
new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){public Void call() throws Exception {
ObjectPayload payloadObj = payloadClass.newInstance();
Object payload = payloadObj.getObject(command);
String name = "pwned" + System.nanoTime();
Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);
try {
registry.bind(name, remote); // 这里使用的是bind,不需要知道远程对象具体的接口,而lookup需要知道具体的参数类型
} catch (Throwable e) {
e.printStackTrace();
}
Utils.releasePayload(payloadObj, payload);
return null;
}});
}

然后是JRMPClient这是对RMIClient的攻击,跟注册中心很类似,这个是攻击DGC服务,跟上面这个攻击的条件都是一样的。

接着是JRMPListener,我们让服务端向客户端发起一个请求,这个时候客户端就会发起一个JRMP攻击,然后我们可以用他来制造一个恶意服务端,来导致JRMP客户端也就是这个真正的服务端/注册中心来执行代码,这个也可以用来反制,用来攻击普通的JRMP客户端。这个就是一个伪造的RMI服务,如果作为一个RMI客户端连了就会被攻击。

在这个payloads里有一个JRMPListener,这个比较少用,就是在一个普通的反序列化点里,得先有一个反序列化点了,然后传一个UnicastRef对象,他反序列化的时候会暴露一个RMI接口出来,他会把一个普通的反序列化点转换成一个RMI的反序列化点,作用就是可能会绕过一些过滤。

然后就是payloads里的JRMPClient,这个是很常用的,是整个RMI分析里最重要的一个链,前面哦们讲的所有东西都是针对RMI服务去打的,他的目标就是RMI服务,如果服务器没有开RMI,要么就是通过刚刚这个JRMPListener给他开一个,没开的话也可以通过这个JRMPClient他也是一个二次反序列化链,但是这个的利用条件要比JRMPListener要宽,这个链放的不是UnicastRef他放的是一个RemoteObjectInvocationHandler,但实际上也是用了UnicastRef的反序列化,区别就是非RMI的反序列化时我们也能用这个payload,让他开一个客户端请求,再用exploit接。这就是一个非常常用的一个二次反序列化链,用来绕过各种服务端的限制,比如shiro这些。同时他还有一个特点,他是从一个输入流的readExternal方法然后把它变成了一个readObject方法可以在这两个之间转换,在别的利用链里也能用到