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; stream.saveRef(ref); if (isResultStream) { 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
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方法,最终让服务器发送客户端请求
实战分析 到这里只是分析了高版本绕过可以调用的链子
具体的实战分析主要可以查看此文章:
RMI:绕过JEP290——上
RMI:绕过JEP290——中
RMI:绕过JEP290——下