<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    深入理解 RMI 之運行邏輯與漏洞原理

    VSole2023-01-29 09:34:56

    深入理解 RMI 之漏洞原理篇

    • 環境是 jdk8u65
    • 本文側重于理解原理,攻擊篇會放到后續一篇中講。

    0x01 前言

    RMI 作為后續漏洞中最為基本的利用手段之一,學習的必要性非常之大。本文著重偏向于 RMI 通信原理的理解,如果只懂利用,就太腳本小子了。

    這里有個坑點:就是 RMI 當中的攻擊手法只在 jdk8u121 之前才可以進行攻擊,因為在 8u121 之后,bind rebind unbind 這三個方法只能對 localhost 進行攻擊,后續我們會提到。

    0x02 RMI 基礎

    1. RMI 介紹

    RMI 全稱 Remote Method Invocation(遠程方法調用),即在一個 JVM 中 Java 程序調用在另一個遠程 JVM 中運行的 Java 程序,這個遠程 JVM 既可以在同一臺實體機上,也可以在不同的實體機上,兩者之間通過網絡進行通信。

    RMI 依賴的通信協議為 JRMP(Java Remote Message Protocol,Java 遠程消息交換協議),該協議為 Java 定制,要求服務端與客戶端都為 Java 編寫。

    • 這個協議就像 HTTP 協議一樣,規定了客戶端和服務端通信要滿足的規范。
    RMI 包括以下三個部分

    Server ———— 服務端:服務端通過綁定遠程對象,這個對象可以封裝很多網絡操作,也就是 Socket
    Client ———— 客戶端:客戶端調用服務端的方法

    因為有了 C/S 的交互,而且 Socket 是對應端口的,這個端口是動態的,所以這里引進了第三個 RMI 的部分 ———— Registry 部分。

    • Registry ———— 注冊端;提供服務注冊與服務獲取。即 Server 端向 Registry 注冊服務,比如地址、端口等一些信息,Client 端從 Registry 獲取遠程對象的一些信息,如地址、端口等,然后進行遠程調用。

    2. RMI 的實現

    • 這里最好把服務端與客戶端拆分成兩個工程來做,會更有助于理解。

    先來寫服務端 ———— Server

    服務端

    1. 先編寫一個遠程接口,其中定義了一個 sayHello() 的方法

    public interface RemoteObj extends Remote {  
      
        public String sayHello(String keywords) throws RemoteException;  
    }
    

    此遠程接口要求作用域為 public;

    繼承 Remote 接口;

    讓其中的接口方法拋出異常

    2. 定義該接口的實現類 Impl

    public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj { 
      
        public RemoteObjImpl() throws RemoteException {  
        //    UnicastRemoteObject.exportObject(this, 0); // 如果不能繼承 UnicastRemoteObject 就需要手工導出  
     }  
      
        @Override  
     public String sayHello(String keywords) throws RemoteException {  
            String upKeywords = keywords.toUpperCase();  
     System.out.println(upKeywords);  
     return upKeywords;  
     }  
    }
    
    • 實現遠程接口
    • 繼承 UnicastRemoteObject 類,用于生成 Stub(存根)和 Skeleton(骨架)。這個在后續的通信原理當中會講到
    • 構造函數需要拋出一個RemoteException錯誤
    • 實現類中使用的對象必須都可序列化,即都繼承java.io.Serializable

    3. 注冊遠程對象

    public class RMIServer {  
        public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {  
            // 實例化遠程對象  
     RemoteObj remoteObj = new RemoteObjImpl();  
     // 創建注冊中心  
     Registry registry = LocateRegistry.createRegistry(1099);  
     // 綁定對象示例到注冊中心  
     registry.bind("remoteObj", remoteObj);  
     }  
    }
    
    • port 默認是 1099,不寫會自動補上,其他端口必須寫
    • bind 的綁定這里,只要和客戶端去查找的 registry 一致即可。

    如此,服務端就寫好了

    客戶端

    客戶端只需從從注冊器中獲取遠程對象,然后調用方法即可。當然客戶端還需要一個遠程對象的接口,不然不知道獲取回來的對象是什么類型的。

    所以在客戶端這里,也需要定義一個遠程對象的接口:

    public interface RemoteObj extends Remote {  
      
        public String sayHello(String keywords) throws RemoteException;  
    }
    

    然后編寫客戶端的代碼,獲取遠程對象,并調用方法

    public class RMIClient {  
        public static void main(String[] args) throws Exception {  
            Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);  
     RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj");  
     remoteObj.sayHello("hello");  
     }  
    }
    

    這樣就能夠從遠端的服務端中調用 RemoteHelloWorld 對象的sayHello()方法了。

    0x03 從 Wireshark 抓包分析 RMI 通信原理

    • 這里文章大部分是引用其他師傅的,我們可以先通過 Wireshark 的抓包心里有個底。

    數據端與注冊中心(1099 端口)建立通訊

    • 客戶端查詢需要調用的函數的遠程引用,注冊中心返回遠程引用和提供該服務的服務端 IP 與端口。

    數據端與注冊中心(1099 端口)建立通訊完成后,RMI Server 向遠端發送了?個 “Call” 消息,遠端回復了?個 “ReturnData” 消息,然后 RMI Server 端新建了?個 TCP 連接,連到遠端的 33769 端?

    AC ED 00 05是常見的 Java 反序列化 16 進制特征

    注意以上兩個關鍵步驟都是使用序列化語句

    客戶端新起一個端口與服務端建立 TCP 通訊

    客戶端發送遠程引用給服務端,服務端返回函數唯一標識符,來確認可以被調用

    同樣使用序列化的傳輸形式

    以上兩個過程對應的代碼是這兩句

    Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);  
    RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj"); // 查找遠程對象
    

    這里會返回一個 Proxy 類型函數,這個 Proxy 類型函數會在我們后續的攻擊中用到。

    客戶端序列化傳輸調用函數的輸入參數至服務端

    • 這一步的同時:服務端返回序列化的執行結果至客戶端

    以上調用通訊過程對應的代碼是這一句

    remoteObj.sayHello("hello");
    

    可以看出所有的數據流都是使用序列化傳輸的,那必然在客戶端和服務帶都存在反序列化的語句。

    總結一下 RMI 的通信原理

    實際建?了兩次 TCP 連接,第一次是去連 1099 端口的;第二次是由服務端發送給客戶端的。

    在第一次連接當中,是客戶端連 Registry 的,在其中尋找 Name 為 hello 的對象,這個對應數據流中的 Call 消息;然后 Registry 返回?個序列化的數據,這個就是找到的Name=Hello的對象,這個對應數據流中的ReturnData消息。

    到了第二次連接,服務端發送給客戶端 Call 的消息。客戶端反序列化該對象,發現該對象是?個遠程對象,地址在 172.17.88.209:24429,于是再與這個地址建? TCP 連接;在這個新的連接中,才執?真正遠程?法調?,也就是sayHello()

    RMI Registry 就像?個?關,他??是不會執?遠程?法的,但 RMI Server 可以在上?注冊?個 Name 到對象的綁定關系;RMI Client 通過 Name 向 RMI Registry 查詢,得到這個綁定關系,然后再連接 RMI Server;最后,遠程?法實際上在 RMI Server 上調?。

    原理圖如圖

    那么我們可以確定 RMI 是一個基于序列化的 Java 遠程方法調用機制。

    0x04 從 IDEA 斷點分析 RMI 通信原理

    • RMI 的這個流程是相當復雜的,需要師傅們有一定的耐心看下去。

    1. 流程分析總覽

    首先 RMI 有三部分:

    • RMI Registry
    • RMI Server
    • RMI Client

    如果兩兩通信就是 3+2+1 = 6 個交互流程,還有三個創建的過程,一共是九個過程。

    RMI 的工作原理可以大致參考這張圖,后續我會一一分析。

    2. 創建遠程服務

    > 先行說明,創建遠程服務這一塊是不存在漏洞的。

    斷點打在 RMIServer 的創建遠程對象這里,如圖

    發布遠程對象

    開始調試,首先是到遠程對象的構造函數RemoteObjImpl,現在我們要把它發布到網絡上去,我們要分析的是它如何被發布到網絡上去的

    RemoteObjImpl這個類是繼承于UnicastRemoteObject的,所以先會到父類的構造函數,父類的構造函數這里的 port 傳入了 0,它代表一個隨機端口。

    這個過程不同于注冊中心的 1099 端口,這是遠程服務的。有很多文章在這個地方都交代的不清楚,誤導了一些師傅。

    遠程服務這里如果傳入的是 0,它會被發布到網絡上的一個隨機端口,我們可以繼續往下看一看。先 f8 到exportObject(),再 f7 跳進去看。

    exportObject()是一個靜態函數,它就是主要負責將遠程服務發布到網絡上,如何更好理解exportObject()的作用呢?我們可以看到RemoteObjImpl這個實現類的構造函數里面,我注銷了一句代碼

    public RemoteObjImpl() throws RemoteException {  
    //     UnicastRemoteObject.exportObject(this, 0); // 如果不能繼承 UnicastRemoteObject 就需要手工導出  
     }
    

    如果不繼承UnicastRemoteObject這個類的話,我們就需要手動調用這個函數。

    我們來看這個靜態函數,第一個參數是 obj 對象,第二個參數是new UnicastServerRef(port),第二個參數是用來處理網絡請求的。繼續往下面跟,去到了UnicastServerRef的構造函數。這里跟的操作先 f7,然后點擊UnicastServerRef跟進,這是 IDEA 的小技巧。

    跟進去之后 UnicastServerRef 的構造函數,我們看到它 new 了一個 LiveRef(port),這個非常重要,它算是一個網絡引用的類,跟進看一看。

    跟進去之后,先是一個構造函數,先跳進 this 看一看

    跳進 this 后的構造函數如下

    public LiveRef(ObjID objID, int port) {  
        this(objID, TCPEndpoint.getLocalEndpoint(port), true);  
    }
    

    第一個參數 ID,第三個參數為 true,所以我們重點關注一下第二個參數。

    TCPEndpoint 是一個網絡請求的類,我們可以去看一下它的構造函數,傳參進去一個 IP 與一個端口,也就是說傳進去一個 IP 和一個端口,就可以進行網絡請求。

    繼續 f7 進到 LiveRef 的構造函數 this 里面

    這時候我們可以看一下一些賦值,發現 host 和 port 是賦值到了 endpoint 里面,而 endpoint 又是被封裝在 LiveRef 里面的,所以記住數據是在 LiveRef 里面即可,并且這一 LiveRef 至始至終只會存在一個。

    上述是 LiveRef 創建的過程,然后我們再回到之前出現LiveRef(port)的地方

    回到上文那個地方,繼續 f7 進入 super 看一看它的父類UnicastRef,這里就證明整個創建遠程服務的過程只會存在一個 LiveRef。一路 f7 到一個靜態函數exportObject(),我們后續的操作過程都與exportObject()有關,基本都是在調用它,這一段不是很重要,一路 f7 就好了。直到此處出現 Stub

    這里在我們服務端創建遠程服務這一步居然出現了 stub 的創建,其實原理是這個樣子的,來結合這張圖一起說:

    • RMI 先在 Service 的地方,也就是服務端創建一個 Stub,再把 Stub 傳到 RMI Registry 中,最后讓 RMI Client 去獲取 Stub。
    接著我們研究 Stub 產生的這一步,先進到 createProxy 這個方法里面

    先進行了基本的賦值,然后我們繼續 f8 往下看,去到判斷的地方。

    這個判斷暫時不用管,后續我們會碰到,那個時候再講。

    再往下走,我們可以看到這是很明顯的類加載的地方

    第一個參數是 AppClassLoader,第二個參數是一個遠程接口,第三個參數是調用處理器,調用處理器里面只有一個 ref,它也是和之前我們看到的 ref 是同一個,創建遠程服務當中永遠只有一個 ref。

    此處就把動態代理創建好了,如圖 Stub

    繼續 f8,到 Target 這里,Target 這里相當于一個總的封裝,將所有用的東西放到 Target 里面,我們可以進去看一看 Target 里面都放了什么。

    并且這里的幾個 ref 都是同一個,通過 ID 就可以查看到它們是同一個。比如比較 disp 和 stub 的。一個是服務端 ,一個是客戶端的,ID 是一樣的,都是 818

    一路 f8,回到之前的 Target,下一條語句是ref.exportObject(target),也就是把 target 這個封裝好了的對象發布出去。

    我們跟進去看一下它的發布邏輯是怎么一回事,一路 f7 到這里

    從這里開始,第一句語句 listen,真正處理網絡請求了跟進去。

    先獲取 TCPEndpoint,然后我們繼續 f8 往后看,直到server = ep.newServerSocket();這里。

    它創建了一個新的 socket,已經準備好了,等別人來連接,所以之后在 Thread 里面去做完成連接之后的事兒,這里我掛幾張圖展示一下運行的邏輯。

    并且這個newServerSocket()方法會給 port 進行賦值,核心語句如圖

    然后回到 listen 去,一路 f8,觀察一下整個流程結束之后 Target 里面是增加了 port。

    發布完成之后的記錄

    • 也就是記錄一下遠程服務被發到哪里去了。

    第一個語句target.setExportedTransport(this);是一個簡單的賦值,我們就不看了,看下面的ObjectTable.putTarget(target);,跟進去,一路 f8,因為都是一些賦值的語句,直到此處。

    RMI 這里會把所有的信息保存到兩個 table里面,有興趣的師傅可以跟一下進去看看。

    我個人理解這段東西有點像日志。

    小結一下創建遠程服務

    從思路來說是不難的,也就是發布遠程對象,用exportObject()指定到發布的 IP 與端口,端口的話是一個隨機值。至始至終復雜的地方其實都是在賦值,創建類,進行各種各樣的封裝,實際上并不復雜。

    還有一個過程就是發布完成之后的記錄,理解的話,類似于日志就可以了,這些記錄是保存到靜態的 HashMap 當中。

    這一塊是服務端自己創建遠程服務的這么一個操作,所以這一塊是不存在漏洞的。

    3. 創建注冊中心 + 綁定

    • 創建注冊中心與服務端是獨立的,所以誰先誰后無所謂,本質上是一整個東西。

    斷點打在此處,開始調試

    創建注冊中心

    首先會經過一個靜態方法 ————createRegistry,繼續往下,走到了RegistryImpl這個對象下,f8 進去,會發現新建了一個RegistryImpl對象。這里 122 行,判斷 port 是否為注冊中心的 port,以及是否開啟了 SecurityManager,也就是一系列的安全檢查,這部分不是很重要,繼續 f8

    再往下走,它創建了一個LiveRef,以及創建了一個新的UnicastServerRef,這段代碼就和我們上面講的創建遠程對象是很類似的,我們可以跟進setup看一下。

    跟進之后發現和之前是一樣的,也是先賦值,然后進行exportObject()方法的調用。

    我這里貼兩張圖,第一張是發布遠程對象的,第二張是創建注冊中心的,師傅們可以對比對比。

    • 區別在于第三個參數的不同,名為 permanent,第一張是 false,第二張是 true,這代表我們創建注冊中心這個對象,是一個永久對象,而之前遠程對象是一個臨時對象。

    f7 進到 exportObject,就和發布遠程對象一樣,到了創建 Stub 的階段。

    • 那這個 Stub 是怎么創建的呢?誒 ~ 這里就和前面的有大不一樣了。我們還是跟進createProxy()中,首先這里要做一個判斷。

    可以跟進stubClassExists進行判斷,我們看到這個地方,是判斷是否能獲取到RegistryImpl_Stub這個類,換句話說,也就是若RegistryImpl_Stub這個類存在,則返回 True,反之 False。我們可以找到RegistryImpl_Stub這個類是存在的。

    • 對比發布遠程對象那個步驟,創建注冊中心是走進到createStub(remoteClass, clientRef);進去的,而發布遠程對象則是直接創建動態代理的。

    執行的這個方法也很簡單,就是直接通過反射創建這個對象,里面放的就是 ref

    相比于之前發布遠程對象中的 Stub,是一個動態代理,里面放的是一個 ref。

    現在發布遠程對象是用 forName 創建的,里面放的也是 ref,是一致的。

    繼續往下,如果是服務端定義好的,就調用setSkeleton()方法,跟進去。然后這里有一個createSkeleton()方法,一看名字就知道是用來創建 Skeleton 的,而 Skeleton 在我們的那幅圖中,作為服務端的代理。

    Skeleton 是用forName()的方式創建的,如圖。

    • 再往后走,又到了 Target 的地方,Target 部分的作用也與之前一樣,用于儲存封裝的數據

    • 所以這一段和前面一樣,就迅速跳過了,到如圖這個地方。

    繼續走,直到super.exportObject(target);這里,f7 跟進,到里面有一個putTarget()方法,它會把封裝的數據放進去。

    一路 f8,到后面看一下到底放了什么東西進去。

    查看封裝了哪些數據進去

    查看 static 中的數據,點開objTable中查看三個 Target,我們逐個分析一下,分析的話主要還是看 ref ~

    先點開這個 Target@930 的 value,主要關注幾個參數:disp 中的 skel,以及 stub。它們的端口都是 1099,也就是說 1099 注冊中心的一些端口數據都有了。這兩個 ref 是同一個,可以對比著看一下。

    先點開這個 Target@1065 的 value,存儲里面需要我們關注的有 stub 是$Proxy對象的,如圖查看它們的 ref

    再點開 Target@1063 的 value 的 stub 值,發現它為 DGCImpl_Stub,是分布式垃圾回收的一個對象,它并不是我們剛才創建的。這個東西挺重要的。

    所以這里就是起了幾個遠程服務,一個端口是固定了,另外兩個端口是不固定的,隨機產生的。至于為什么這里有三個 Target 呢?

    • 這個我們在第六點里面會講到。

    綁定

    • 綁定也就是最后一步,bind 操作

    斷點下在 bind 語句那里。我們開始調試

    首先檢查是否是本地綁定的,有興趣的師傅們可以跟一下,是都會通過的,我這里就不跟了

    下一句檢查一下 bindings 這里面是否有東西,其實 bindings 就是一個 HashTable。如果里面有數據的話就拋出異常。

    繼續往前走,就是bindings.put(name, obj);,也挺好理解的,就是把 IP 和端口放進去,到此處,綁定過程就結束了hhhh,是最簡單的一個過程。

    小結一下創建注冊中心 + 綁定

    • 總結一下比較簡單,注冊中心這里其實和發布遠程對象很類似,不過多了一個持久的對象,這個持久的對象就成為了注冊中心。

    綁定的話就更簡單了,一句話形容一下就是hashTable.put(IP, port)

    3. 客戶端請求,客戶端調用注冊中心

    • 這一部分是存在漏洞的點,原因很簡單,因為前文我們在 Wireshark 的抓包里頭說到:"RMI 是一個基于序列化的 Java 遠程方法調用機制",這里有一些個有問題的反序列化 ~
    • 且聽我娓娓道來

    客戶端的請求分為三個階段,獲取注冊中心,查找對象,

    獲取注冊中心

    這一塊不存在漏洞,我們可以調試看一下,很簡單。

    斷點的話,三句代碼都先下斷點,接著開始調試。

    進到getRegistry()方法里面,繼續往下走,這里調試部分大家可以自己看一下,都不難的,無非是一些賦值與判斷,大致流程其實和之前是很像的,有new LiveRef的操作,有Util.createProxy()的操作,感興趣的師傅們可以跟進去看一下,是一樣的流程。也是通過forName的方式創建的。

    就和之前一樣,新建了一個 Ref,然后把該封裝的都封裝到 Ref 里面進去。這里封裝的是127.0.01:1099的,這里我們就獲取到了注冊中心的 Stub,下一步就是去查找遠程對象。

    查找遠程對象

    • 這里調試的話,因為對應的 Java 編譯過的 class 文件是 1.1 的版本,無法進行打斷點,所以會直接跳到其他地方去,比如此處。

    代碼是可以按照正常的邏輯走的,就是打不了斷點,問題不大,我們主要分析一下代碼運行的邏輯。

    先看我們變量里面多了一個param_1="remoteObj",這個東西就是傳參的 String var1,這個 var1 最后是作為序列化的數據傳進去的。注冊中心后續會通過反序列化讀取。

    接著下一步,我們看到super.ref.invoke(var2);,super 就是父類,也就是我們之前說的UnicastRef這個類。這里的invoke()方法是類似于激活的方法,invoke()方法里面會調用call.executeCall(),它是真正處理網絡請求的方法,也就是客戶端的網絡請求都是通過這個方法實現的。

    • 這個方法后續再細講,先看整個代碼運行的邏輯。

    我們的邏輯現在是從invoke()--->call.executeCall()--->out.getDGCAckHandler(),到out.getDGCAckHandler()這個地方的時候,是 try 打頭的,這里它有一個異常存在潛在攻擊的可能性,如圖,中間省略了部分代碼。

    我們先看一下in這個變量是什么

    不難理解,in 就是數據流里面的東西。這里獲取異常的本意應該是在報錯的時候把一整個信息都拿出來,這樣會更清晰一點,但是這里就出問題了 ———— 如果一個注冊中心返回一個惡意的對象,客戶端進行反序列化,這就會導致漏洞。這里的漏洞相比于其他漏洞更為隱蔽。

    • 也就是說,只要調用invoke(),就會導致漏洞。RMI 在設計之初就并未考慮到這個問題,導致客戶端都是易受攻擊的。

    上述就是注冊中心與客戶端進行交互時會產生的攻擊。

    我們這里繼續 f8,看一下到最后一步的時候獲取到了什么數據。簡單來說就是獲取到了 RemoteObj 這個動態代理,其中包含一個 ref。

    4. 客戶端請求,客戶端請求服務端

    存在漏洞

    這里就是客戶端請求的第三句代碼 ————remoteObj.sayHello("hello");的運行邏輯。

    這里如果 Debug 有問題的話,可以先在RemoteObjectInvocationHandler類下的invoke()方法的 if 判斷里面打個斷點,這樣才能走進去。調試開始

    下面是一堆 if 的判斷,都是關于拋出異常的,這里就不再細看了,直接跳過。直到尾部這個地方,我們跟進去看一下。

    跟進到此處,ref.invoke(),這是一個重載的方法,跟進到重載的invoke()方法里面。這個重載的invoke方法作用是創建了一個連接,和之前也比較類似。我們可以看一下它具體的邏輯實現。

    繼續往里走,在循環里面有一個marshalValue()方法。

    它會序列化一個值,這個值其實就是我們傳進的參數hello,它的邏輯如圖。判斷一堆類型,之后再進行序列化。

    繼續往前走,我們看到一個注釋// unmarshal return,后面接的是call.executeCall(),之前我們也看到了這個方法,也就是說只要 RMI 處理網絡請求,就一定會執行到這個方法,這里是存在危險的,原理上面已經代碼跟過一遍了 ~

    所以我們直接往后看。

    • 這里有一個unmarshalValueSee的方法,因為現在我們傳進去的類型是 String,不符合上面的一系列類型,這里會進行反序列化的操作,把這個數據讀回來,這里是存在入口類的攻擊點的。

    這個數據會被讀回來

    關于客戶端一系列主動請求的小結

    • 先說說存在攻擊的點吧,在注冊中心 --> 服務端這里,查找遠程對象的時候是存在攻擊的。

    具體表現形式是服務端打客戶端,入口類在call.executeCall(),里面拋出異常的時候會進行反序列化。

    這里可以利用 URLClassLoader 來打,具體的攻擊在后續文章會寫。

    在服務端 ---> 客戶端這里,也是存在攻擊的,一共是兩個點:一個是call.executeCall(),另一個點是unmarshalValueSee這里。

    • 再總結一下代碼的流程

    分為三步走,先獲取注冊中心,再查找遠程對象,查找遠程對象這里獲取到了一個 ref,最后客戶端發出請求,與服務端建立連接,進行通信。

    5. 客戶端發起請求,注冊中心如何處理

    先說說斷點怎么打,因為客戶端那里,我們操作的是 Stub,服務端這邊操作的是 Skel。在有了 Skel 之后應當是存在 Target 里面的,所以我們的斷點打到處理 Target 的地方。

    斷點位置如圖

    先點 Server 的 Debug,再跑 Client 就可以了,成功的打斷點如圖

    往下走,我們先看一看 Target 里面包含了什么

    里面包含一個 stub,stub 中是一個 ref,這個 ref 對應的是 1099 端口。

    再往下走final Dispatcher disp = target.getDispatcher();是將skel的值放到 disp 里面。

    繼續往下走,它會調用 disp 的 dispatch 方法,我們跳進去看一下disp.dispatch()

    繼續走,我們目前的skel不為 null,會到oldDispatch()這里,跟進。

    下面就是skel.dispatch()的過程了,這里才是重點,這里就是很多師傅文章里面會提到的 客戶端打注冊中心的攻擊方式。

    • 先介紹一下這段源碼吧,很長,基本都是在做 case 的工作。

    我們與注冊中心進行交互可以使用如下幾種方式:

    • list
    • bind
    • rebind
    • unbind
    • lookup

    這幾種方法位于RegistryImpl_Skel#dispatch中,也就是我們現在 dispatch 這個方法的地方。

    如果存在對傳入的對象調用readObject方法,則可以利用,dispatch里面對應關系如下:

    • 0->bind
    • 1->list
    • 2->lookup
    • 3->rebind
    • 4->unbind

    只要中間是有反序列化就是可以攻擊的,而且我們是從客戶端打到注冊中心,這其實是黑客們最喜歡的攻擊方式。我們來看一看誰可以攻擊。

    也就是除了 list 都可以。

    小結一下客戶端發起請求,注冊中心做了什么

    簡單,注冊中心就是處理 Target,進行 Skel 的生成與處理。

    漏洞點是在 dispatch 這里,存在反序列化的入口類。這里可以結合 CC 鏈子打的。

    6. 客戶端發起請求,服務端做了什么

    • 這個流程是比較簡單的,同第四點一樣,此處得到的 Skel 是動態代理$Proxy0這個類的,之前我們提到過其實是封裝了三個 Target 的,這就是其中之一。

    這里的斷點位置打兩個,如圖:

    也就是當前請求到的是服務端的 Target,我們開始調試。

    • 調試這里有一點小坑,打完兩個斷點之后,我們得到的第一個 Target 中的 Stub 是 DGCImpl 的,我們要的不是這個,前文我們提到過,這個類是用來處理內存垃圾的。

    動態代理的 stub

    這里要摁兩下一下 f9,直至有 Proxy 動態代理的 stub 為止,如圖:

    在這種情況下,我們到dispatch()方法下,跟進。

    這里的 skel 為 null,所以不會執行 oldDispatch 方法,如圖

    繼續往下走,獲取到輸入流,以及 Method,Method 就是我們之前寫的sayHello()方法。

    繼續往下走,重點部分來了 ———— 循環當中的unmarshalValue()方法,這里和我們之前說的一樣,是存在漏洞的。

    這里的流程和之前是一致的,也就是我們的"hello"傳參傳進去,序列化讀進去,反序列化讀出來,和之前是一致的。

    DGC 的 stub

    三個 Target 當中的一個,如圖

    斷點需要下在ObjectTable類的putTarget()方法里面。并且將前面兩個斷點去掉,直接調試即可。

    • 首先我們去看一看 DGC 的運行原理是什么

    還是比較簡單的,將 Target 放到一個靜態表里面,這里靜態表就是我在第三點說的,ObjectTable 里面封裝了三個 Target。

    然后這里我們會發現,放進去的是 Proxy 這個動態代理的 Target 而非 DGC 的 Target。

    這個 DGC 的 Target 挺奇妙的,是已經被封裝到了 static 里面,我們去看 static 里面,發現它已經被封裝進去了。

    • 那它到底是怎么創建的呢?我們一步步看。
    在 DGC 這個類在調用靜態變量的時候,就會完成類的初始化。

    類的初始化是由 DGCImpl 這個類完成的,我們跟到 DGCImpl 中去看,發現里面有一個 static 方法,作用是 class initializer

    • 我們可以在創建對象的地方打個斷點。

    后續的過程,首先是 new 了很多對象,這些其實都是 Target 的一堆屬性,不過這是封裝之前的。

    后續的部分,createProxy()方法這里,和注冊中心創建遠程服務的特別像。

    createProxy()方法進去,會看到一個createStub()方法,跟進去。

    這里和注冊中心創建遠程服務一樣,嘗試是否可以獲取到這一個類 ————DGCImpl_Stub

    這一個 DGCImpl_Stub 的服務至此已經被創建完畢了,它也是類似于創建遠程服務一樣,但是它做的業務不一樣。注冊中心的遠程服務是用于注冊的,這個是用于內存回收的,且端口隨機。

    setSkeleton()這個過程就是在 disp 里面創建skel,和之前是一樣的。

    調用過程是與第 3、4 點講的一樣的,這里就不重復了。

    我們重點關注一下 DGC 的 Stub 里面有漏洞的地方。

    DGCImpl_Stub這個類下,它有兩個方法,一個是 clean,另外一個是 dirty。clean 就是"強"清除內存,dirty 就是"弱"清除內存。

    這里調用了readObject()方法,存在反序列化的入口類。

    同樣在DGCImpl_Skel這個類下也存在反序列化的漏洞,如圖。

    小結一下 DGC 的過程

    • 是自動創建的一個過程,用于清理內存。

    漏洞點在客戶端與服務端都存在,存在于SkelStub當中。這也就是所謂的 JRMP 繞過

    0x05 總結

    • 如果是漏洞利用的話,單純攻擊 RMI 意義是不大的,不論是 codespace 的那種利用,難度很高,還是說三者互相打這種,意義都不是很大,因為在 jdk8u121 之后都基本修復完畢了。

    RMI 多數的利用還是在后續的 fastjson,strust2 這種類型的攻擊組合拳比較多,希望這篇文章能對正在學習 RMI 的師傅們提供一點幫助。

    具體的攻擊 payload 可以看我另外一篇文章

    0x06 參考資料

    https://www.bilibili.com/video/BV1L3411a7ax?p=10&vd_source=a4eba559e280bf2f1aec770f740d0645

    https://johnfrod.top/%e5%ae%89%e5%85%a8/rmi%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96/

    target遠程過程調用
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    通常我們在滲透過程中從外圍打點進入內網后拿到主機提升到system權限,這一臺主機就已經拿下。但是我們進入內網的目標還是拿下盡可能多的主機,這時候選擇橫向移動的方法就尤為重要。今天就對一些常用的橫向手法進行一個總結,有不足之處歡迎師傅們進行斧正。
    2020年08月11日,Windows官方發布了 NetLogon 特權提升漏洞的風險通告,該漏洞編號為CVE-2020-1472,漏洞等級:嚴重,漏洞評分:10分,該漏洞也稱為“Zerologon”,2020年9月11日,Secura高級安全專家Tom Tervoort和技術總監Ralph Moonen發表了漏洞相關的博文和白皮書,分享了Zerologon漏洞的細節,表示攻擊者在通過NetLogon協議與AD域控建立安全通道時,可利用該漏洞將AD域控的計算機賬號密碼置為空,從而控制域控服務器,之后相關的EXP也就被開發出來了。
    所以可以通過它傳回lsass.dmp本地提取hashprocdump64.exe -accepteula -ma lsass.exe lsass.dmp 執行該指令,獲取到lsass.dmp
    所以可以通過它傳回lsass.dmp本地提取hashprocdump64.exe -accepteula -ma lsass.exe lsass.dmp 執行該指令,獲取到lsass.dmp
    如果找到了某個用戶的ntlm hash,就可以拿這個ntlm hash當作憑證進行遠程登陸了 其中若hash加密方式是 rc4 ,那么就是pass the hash 若加密方式是aes key,那么就是pass the key 注意NTLM和kerberos協議均存在PTH: NTLM自然不用多說 kerberos協議也是基于用戶的client hash開始一步步認證的,自然也會受PTH
    大多數計算機系統設計為可與多個用戶一起使用。特權是指允許用戶執行的操作。普通特權包括查看和編輯文件或修改系統文件。特權升級意味著用戶獲得他們無權獲得的特權。這些特權可用于刪除文件,查看私人信息或安裝不需要的程序,例如病毒。
    一文吃透 Linux 提權
    2021-10-23 07:09:32
    特權升級意味著用戶獲得他們無權獲得的特權。通常,當系統存在允許繞過安全性的錯誤或對使用方法的設計假設存在缺陷時,通常會發生這種情況。結果是,具有比應用程序開發人員或系統管理員想要的特權更多的應用程序可以執行未經授權的操作。
    該漏洞是繼CVE-2015-4852、CVE-2016-0638、CVE-2016-3510之后的又一個重量級反序列化漏洞。
    很多人把這個原因歸結于KB2871997補丁,實際上不然,這個事情的成因實際是UAC在搗亂。RID為500的賬戶和屬于本地administrators組的域用戶在通過網絡遠程鏈接時,默認就是高權限令牌。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类