没人比我更懂RMI(下)

前言:

终于来到最后一篇了,这个RMI确实拖了我很长时间,一直没时间写,还发现有很多新的东西需要写,但是自己又没有时间,,,,后面还有类加载、Fastjson、CC、CB,,,,,,,感觉自己还有好多东西要学,大脑有点不够用了(猪脑过载),还是慢慢积累,慢慢成长吧。噢,对了上次忘了每篇鸡汤,这次一并补上:

1
2
3
4
路漫漫其修远兮,吾将上下而求索
——屈原
千磨万击还坚韧,任尔东南西北风
——郑板桥

攻击客户端:

注册中心攻击客户端

img

通过图片我们可以看到注册中心与客户端之间通信是为了获取stub。

那么我们下断点跟一下看看里面究竟藏着什么猫腻。

image-20230825201413522

直接来到RegistryImpl_Stub.lookup

image-20230825202452411

进来之后一眼看到三部分1,3部分很简单就是先将传入的字符序列化传给注册中心,最后再把注册中心返回的数据进行反序列化。

这个反序列化的点就是我们可以利用的点,假如注册中心返回一个恶意类,客户端反序列化之后会直接执行。所以这就是我们攻击客户端的第一种方法

还有比较有意思的第2部分:

image-20230825202810211

可以看到它是UnicastRef.invoke(var2)具体的值也可以看到,我们再次进行跟进:

image-20230825203034282

再次跟进:

image-20230825203409694

可以看到这里捕获2异常,如果异常为2,则会对流进行反序列化,这里的本意可能是想通过反序列化来获取更详细的错误信息,但是这里实际上也是利用点,且这个利用点更加隐蔽、且范围更广。为什么这么说?

image-20230825203804805

因为只要是调用的这个invoke的函数都会触发反序列化,但是在处理网络请求的时候,会大量的调用这个invoke,所以这里也更危险。

服务端攻击客户端

image-20230826103901123

跟进客户端调取服务端远程对象:

image-20230826104100632

直接来到了invoke方法并调用invokeRemoteMethod(proxy, method, args);可以很清楚的看到堆栈信息和具体传入的参数。我们再次跟进:

image-20230826104547535

可以看到这里又调用了UnicastRef.invoke()但是这里的invoke是重载过的,和我们上面的UnicastRef.invoke()有点不太一样,我们再次跟进:

image-20230826105829638

image-20230826110042875

可以看到这里的marshalValue()其实是将我们输入进行序列化,通过参数信息也可以看到,这里将我们调用的远程方法hello也传入进去。

完了之后又调用了executeCall()也就是说所有客户端的请求都会调用这个executeCall()方法。我们再往后看:

image-20230826110437577

这里会获取到服务端的返回值传入unmarshalValue()而在这个unmarshalValue()中其实是将返回值进行反序列化:

image-20230826110607396

所以看到这我们也明白了,客户端与服务端之间,客户端也是有两个反序列化的点。

首先第一个,细心的同学可以看到上图我在executeCall()打了断点但是没跟进去,原因是因为他和之前(客户端与注册中心)的executeCall()是一样的。他之中也有反序列化的点:

image-20230826111124434

第二个反序列化的点就是我们分析的unmarshalValue()中将服务端返回值反序列化。

攻击注册中心

客户端攻击注册中心

当客户端在请求注册中心时会来到Transport.serviceCall()

image-20230826113208590

可以看到这里先是获取Target,而这个Target也就是RegistryImpl_Stub,后面会获取分发器,而里面的内容就是skel。后面会调用dispatch()

image-20230826113532626

image-20230826113741741

这个dispatch也就是UnicastServerRef我们再次进行跟进:

image-20230826114527833

这里首先是获取输入,在检查skel字段是否为空,不为空则调用oldDispatch(),那我们再跟进:

image-20230826114853814

这里也就终于走到了skel.dispatch(),这个dispatch()也就是我们上一篇讲到的那几种方法对应的case,他会对不同的方法执行不同的readObject。(因为,截不下。我贴一下,这样大家就不用回去看了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
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;
ObjectInput var8;
ObjectInput var9;
Remote var80;
switch (var3) {
case 0:
RegistryImpl.checkAccess("Registry.bind");

try {
var9 = var2.getInputStream();
var7 = (String)var9.readObject();
var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var77) {
throw new UnmarshalException("error unmarshalling arguments", var77);
} finally {
var2.releaseInputStream();
}

var6.bind(var7, var80);

try {
var2.getResultStream(true);
break;
} catch (IOException var76) {
throw new MarshalException("error marshalling return", var76);
}
case 1:
var2.releaseInputStream();
String[] var79 = var6.list();

try {
ObjectOutput var81 = var2.getResultStream(true);
var81.writeObject(var79);
break;
} catch (IOException var75) {
throw new MarshalException("error marshalling return", var75);
}
case 2:
try {
var8 = var2.getInputStream();
var7 = (String)var8.readObject();
} catch (ClassNotFoundException | IOException var73) {
throw new UnmarshalException("error unmarshalling arguments", var73);
} finally {
var2.releaseInputStream();
}

var80 = var6.lookup(var7);

try {
ObjectOutput var82 = var2.getResultStream(true);
var82.writeObject(var80);
break;
} catch (IOException var72) {
throw new MarshalException("error marshalling return", var72);
}
case 3:
RegistryImpl.checkAccess("Registry.rebind");

try {
var9 = var2.getInputStream();
var7 = (String)var9.readObject();
var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var70) {
throw new UnmarshalException("error unmarshalling arguments", var70);
} finally {
var2.releaseInputStream();
}

var6.rebind(var7, var80);

try {
var2.getResultStream(true);
break;
} catch (IOException var69) {
throw new MarshalException("error marshalling return", var69);
}
case 4:
RegistryImpl.checkAccess("Registry.unbind");

try {
var8 = var2.getInputStream();
var7 = (String)var8.readObject();
} catch (ClassNotFoundException | IOException var67) {
throw new UnmarshalException("error unmarshalling arguments", var67);
} finally {
var2.releaseInputStream();
}

var6.unbind(var7);

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

}
}

服务端攻击注册中心

这里就不跟了,流程基本上大差不差。只不过服务端会调用bind,case的点不一样。

攻击服务端

客户端攻击服务端

之后我们要确保获取到的Target是动态代理,因为这样才是服务端在处理请求。

image-20230826120329281

也是会来到Transport.serviceCall(),调用dispatch()

image-20230826115940552

跟进:

image-20230826120750763

我们发现之前注册中心在处理的时候,因为接收到的skel不为空所以走的是上面的if,但是服务端在处理的时候会直接走到下面。

image-20230826121943913

这里会调用这个unmarshalValue()方法,其实这个方法就是将客户端的数据进行反序列化:

image-20230826122123337

最后在进行真正的调用:

image-20230826122303876

看到这利用点也就出来了,就是我们上面的readObject了。

注册中心攻击服务端

同样的套路,我们直接略过。。。。。

至此,我们的RMI也就草草结束了,,其实我只是很浅显的写了一下,包括他还有通过JRMP去攻击客户端、回收机制、高版本绕过、等等。我觉得先浅浅的了解一点,等后面用得着再深入的学这样是好的。然后后面的主线就是去分析一些类似于RMI的这种在java安全中有着一席之地的东西,支线的话偶尔会穿插一些漏洞分析文章。我尽量自律,保持稳定的产出速度(如果没有什么意外发生)。


没人比我更懂RMI(下)
http://example.com/2023/08/26/没人比我更懂RMI3/
作者
Yuanyi
发布于
2023年8月26日
许可协议