RMI之Java高版本绕过
Java高版本限制
在Java高版本中,在RegistryImpl
类中新加了一个registryFilter
方法,里面对所传入的序列化对象的类型进行了限制
1 2 3 4 5 6 7 8 9 10 11 12 if (String.class == clazz || java.lang.Number.class.isAssignableFrom(clazz) || Remote.class.isAssignableFrom(clazz) || java.lang.reflect.Proxy.class.isAssignableFrom(clazz) || UnicastRef.class.isAssignableFrom(clazz) || RMIClientSocketFactory.class.isAssignableFrom(clazz) || RMIServerSocketFactory.class.isAssignableFrom(clazz) || java.rmi.server.UID.class.isAssignableFrom(clazz)) { return ObjectInputFilter.Status.ALLOWED; } else { return ObjectInputFilter.Status.REJECTED; }
绕过分析
调用流程
其中有希望利用的只有Proxy
和UnicastRef
类,其中最重要的是UnicastRef
类,在这个类中有一个invoke
方法 ,在修复以后,客户端的被攻击点是没有被修复的,我们就想如果能让服务端去发送一个客户端请求,就会暴露出攻击点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void invoke (RemoteCall var1) throws Exception { try { clientRefLog.log(Log.VERBOSE, "execute call" ); var1.executeCall(); } catch (RemoteException var3) { clientRefLog.log(Log.BRIEF, "exception: " , var3); this .free(var1, false ); throw var3; } catch (Error var4) { clientRefLog.log(Log.BRIEF, "error: " , var4); this .free(var1, false ); throw var4; } catch (RuntimeException var5) { clientRefLog.log(Log.BRIEF, "exception: " , var5); this .free(var1, false ); throw var5; } catch (Exception var6) { clientRefLog.log(Log.BRIEF, "exception: " , var6); this .free(var1, true ); throw var6; } }
我们的想法是找一个地方去调用Util.createProxy
创建一个动态代理类,我们找到的是DGC这个类可以被利用,然后调用它的clean
或者dirty
方法去触发他的invoke
方法
我们直接走向最终找到的DGCClient
内部类的EndpointEntry
的构造方法方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private EndpointEntry (final Endpoint endpoint) { this .endpoint = endpoint; try { LiveRef dgcRef = new LiveRef (dgcID, endpoint, false ); dgc = (DGC) Util.createProxy(DGCImpl.class, new UnicastRef (dgcRef), true ); } catch (RemoteException e) { throw new Error ("internal error creating DGC stub" ); } renewCleanThread = AccessController.doPrivileged( new NewThreadAction (new RenewCleanThread (), "RenewClean-" + endpoint, true )); renewCleanThread.start(); }
接下来我们找哪里去创建了这么一个类,在EndpointEntry
中的lookup
方法中创建了这么一个类
1 2 3 4 5 6 7 8 9 10 11 12 13 public static EndpointEntry lookup (Endpoint ep) { synchronized (endpointTable) { EndpointEntry entry = endpointTable.get(ep); if (entry == null ) { entry = new EndpointEntry (ep); endpointTable.put(ep, entry); if (gcLatencyRequest == null ) { gcLatencyRequest = GC.requestLatency(gcInterval); } } return entry; } }
我们看只有在registerRefs
中调用了EndpointEntry
的lookup
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 static void registerRefs (Endpoint ep, List<LiveRef> refs) { EndpointEntry epEntry; do { epEntry = EndpointEntry.lookup(ep); } while (!epEntry.registerRefs(refs)); }
我们需要找到一个反序列化利用的点,再向上找,会找到两个调用registerRefs
的方法
我们看read
方法中,如果这个输入流不是并且不继承于ConnectionInputStream
的话,就会调用我们的registerRefs
方法,但是这个in是一个ConnectionInputStream
,所以我们只能去找另一个方法去利用
1 2 3 4 5 6 7 8 9 10 11 12 13 if (in instanceof ConnectionInputStream) { ConnectionInputStream stream = (ConnectionInputStream)in; stream.saveRef(ref); if (isResultStream) { stream.setAckNeeded(); } } else { DGCClient.registerRefs(ep, Arrays.asList(new LiveRef [] { ref })); }
另一个最终的流程是releaseInputStream
去调用StreamRemoteCall.registerRefs
然后进入到if中(这个判断条件中的incomingRefTable是为空的,我们后续会说怎么走到if里面)调用DGCClient.registerRefs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void releaseInputStream () throws IOException { try { if (in != null ) { try { in.done(); } catch (RuntimeException e) { } in.registerRefs(); in.done(conn); } conn.releaseInputStream(); } finally { in = null ; } } void registerRefs () throws IOException { if (!incomingRefTable.isEmpty()) { for (Map.Entry<Endpoint, List<LiveRef>> entry : incomingRefTable.entrySet()) { DGCClient.registerRefs(entry.getKey(), entry.getValue()); } } }
最后调用releaseInputStream
的地方就是非常的多了,在许多Skel中都有调用
最后的流程如下,只要调用releaseInputStream
,就会创建一个proxy
对象
incomingRefTable赋值
实际上反序列化流程,只是为了给incomingRefTable赋值,攻击流程实际是在正常的调用流程中
我们上面说过,这里的值默认是空的,要想走入DGCClient.registerRefs
中,我们就应该去找,哪里给incomingRefTable赋值
1 2 3 4 5 6 7 8 void registerRefs () throws IOException { if (!incomingRefTable.isEmpty()) { for (Map.Entry<Endpoint, List<LiveRef>> entry : incomingRefTable.entrySet()) { DGCClient.registerRefs(entry.getKey(), entry.getValue()); } } }
实际这里只有一个地方ConnectionInputStream
的saveRef
中,向里面put进去了一个东西,使他不为空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void saveRef (LiveRef ref) {Endpoint ep = ref.getEndpoint();List<LiveRef> refList = incomingRefTable.get(ep); if (refList == null ) { refList = new ArrayList <LiveRef>(); incomingRefTable.put(ep, refList); } 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; stream.saveRef(ref); if (isResultStream) { stream.setAckNeeded(); } } else { DGCClient.registerRefs(ep, Arrays.asList(new LiveRef [] { ref })); } return ref; } }
我们看看谁调用了read
方法,只有UnicastRef
和UnicastRef2
中的readExternal
去调用了read
方法
readExternal
是一个和readObject
类似但不一样的东西,如果所反序列化的类,也有readExternal
方法,也会去调用readExternal
方法
1 2 3 4 5 public void readExternal (ObjectInput in) throws IOException, ClassNotFoundException { ref = LiveRef.read(in, false ); }
UnicastRef
是白名单里面的内容,我们向客户端传入一个UnicastRef
对象触发它的readexternal
方法
1 2 3 public void readExternal (ObjectInput var1) throws IOException, ClassNotFoundException { this .ref = LiveRef.read(var1, false ); }
进入到LiveRef.read
中… 剩下的调用我们就不再重复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static LiveRef read (ObjectInput var0, boolean var1) throws IOException, ClassNotFoundException { ...... if (var0 instanceof ConnectionInputStream) { ConnectionInputStream var6 = (ConnectionInputStream)var0; var6.saveRef(var5); if (var4) { var6.setAckNeeded(); } } else { DGCClient.registerRefs(var2, Arrays.asList(var5)); } return var5; } ...
后续会走到EndpointEntry
中,在创建完dgc后会走到下面创建一个RenewCleanThread
线程
1 2 3 4 5 6 7 8 9 10 11 12 13 private EndpointEntry (Endpoint var1) { this .endpoint = var1; try { LiveRef var2 = new LiveRef (DGCClient.dgcID, var1, false ); this .dgc = (DGC)Util.createProxy(DGCImpl.class, new UnicastRef (var2), true ); } catch (RemoteException var3) { throw new Error ("internal error creating DGC stub" ); } this .renewCleanThread = (Thread)AccessController.doPrivileged(new NewThreadAction (new RenewCleanThread (), "RenewClean-" + var1, true )); this .renewCleanThread.start(); }
RenewCleanThread
中,会调用DGCClient
的makeDirtyCall
方法,而这个方法最终会调用他的dirty
方法,就会调用到invoke
方法,最终让服务器发送客户端请求
1 2 3 4 5 6 7 8 9 10 11 12 13 AccessController.doPrivileged(new PrivilegedAction <Void>() { public Void run () { if (var4) { EndpointEntry.this .makeDirtyCall(var5, var6); } if (!EndpointEntry.this .pendingCleans.isEmpty()) { EndpointEntry.this .makeCleanCalls(); } return null ; } }