Java安全-RMI之Java高版本绕过

Java安全-RMI之Java高版本绕过

Java高版本限制

在Java高版本中,在RegistryImpl类中新加了一个registryFilter方法,里面对所传入的序列化对象的类型进行了限制

可以看到传入注册中心的String类型是只能在设定好的类之中的

1
2
3
4
5
6
7
8
9
10
11
12
if (String.class == clazz
|| java.lang.Number.class.isAssignableFrom(clazz)
|| Remote.class.isAssignableFrom(clazz)
|| java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
|| UnicastRef.class.isAssignableFrom(clazz)
|| RMIClientSocketFactory.class.isAssignableFrom(clazz)
|| RMIServerSocketFactory.class.isAssignableFrom(clazz)
|| java.rmi.server.UID.class.isAssignableFrom(clazz)) {
return ObjectInputFilter.Status.ALLOWED;
} else {
return ObjectInputFilter.Status.REJECTED;
}

代码分析

如果想要继续攻击,可利用的类只有 Proxy 和 UnicastRef 这两个类

进去到UnicastRef这个类,发现invoke方法是并没有被修复的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public Object invoke(Remote var1, Method var2, Object[] var3, long var4) throws Exception {
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "method: " + var2);
}

if (clientCallLog.isLoggable(Log.VERBOSE)) {
this.logClientCall(var1, var2);
}

Connection var6 = this.ref.getChannel().newConnection();
Object var7 = null;
boolean var8 = true;
boolean var9 = false;

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

StreamRemoteCall var46 = new StreamRemoteCall(var6, this.ref.getObjID(), -1, var4);

try {
ObjectOutput var10 = var46.getOutputStream();
this.marshalCustomCallData(var10);
var11 = var2.getParameterTypes();

for(int var12 = 0; var12 < ((Object[])var11).length; ++var12) {
marshalValue((Class)((Object[])var11)[var12], var3[var12], var10);
}
} catch (IOException var39) {
clientRefLog.log(Log.BRIEF, "IOException marshalling arguments: ", var39);
throw new MarshalException("error marshalling arguments", var39);
}

var46.executeCall();

如果想要攻击服务端,我们想到的是让服务端发起一个客户端请求,这样就有可能在服务端引发一个反序列化攻击

(其实刚听到这种攻击思路是没有理解的)

先要调用invoke函数,是需要一个stub的,需要creatProxy函数去创建

找到的是DGC这个类可以被利用,然后调用它的clean或者dirty方法去触发他的invoke方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private EndpointEntry(final Endpoint endpoint) {
this.endpoint = endpoint;
try {
LiveRef dgcRef = new LiveRef(dgcID, endpoint, false);
dgc = (DGC) Util.createProxy(DGCImpl.class,
new UnicastRef(dgcRef), true);
} catch (RemoteException e) {
throw new Error("internal error creating DGC stub");
}
renewCleanThread = AccessController.doPrivileged(
new NewThreadAction(new RenewCleanThread(),
"RenewClean-" + endpoint, true));
renewCleanThread.start();
}

利用反序列化去创建一个stub,可以利用它在服务端去发起一个客户端请求

现在是要去找一个反序列化入口点,我们开始查找用法

EndpointEntry -> lookup -> registerRefs

到registerRefs这里查找用法时会出现两处调用点

我们看read方法中,如果这个输入流不是并且不继承于ConnectionInputStream的话,就会调用我们的registerRefs方法,但是这个in是一个ConnectionInputStream,在RMI的输入流中基本都是ConnectionInputStream

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

所以我们看到另一个方法调用ConnectionInputStream#registerRefs

1
2
3
4
5
6
void registerRefs() throws IOException {
if (!this.incomingRefTable.isEmpty()) {
for(Map.Entry var2 : this.incomingRefTable.entrySet()) {
DGCClient.registerRefs((Endpoint)var2.getKey(), (List)var2.getValue());
}
}

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

顺着向前查找,发现DGCImpl_Skel中dispatch是会调用这个方法的

不管走到哪个case里,都会调用releaseInputStream方法

incomingRefTable赋值

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

如何调用ConnectionInputStream.registerRefs然后进入到if中

选中incomingRefTable右键查找用法

来到ConnectionInputStream.saveRef -> LiveRef.read -> UnicastRef.readExternal

ConnectionInputStreamsaveRef中,向里面put进去了一个东西,使他不为空

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

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

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

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

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

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

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

return ref;
}
}

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

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

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

具体调用流程

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

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

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

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

return var5;
}

...

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

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

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

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

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

实战分析

到这里只是分析了高版本绕过可以调用的链子

具体的实战分析主要可以查看此文章:

RMI:绕过JEP290——上

RMI:绕过JEP290——中

RMI:绕过JEP290——下


Java安全-RMI之Java高版本绕过
http://huang-d1.github.io/2025/09/26/Java安全-RMI之Java高版本绕过/
作者
huangdi
发布于
2025年9月26日
许可协议