1. 程式人生 > >Java安全之ysoserial-JRMP模組分析(一)

Java安全之ysoserial-JRMP模組分析(一)

# Java安全之ysoserial-JRMP模組分析(一) 首發安全客:[Java安全之ysoserial-JRMP模組分析(一)](https://www.anquanke.com/post/id/228918) ## 0x00 前言 在分析到Weblogic後面的一些繞過方式的時候,分析到一半需要用到ysoserial-JRMP該模組。不止是Weblogic的反序列化漏洞會利用到,其他的反序列化漏洞也會利用到,所以在此對該模組做一個分析。瞭解底層原理,一勞永逸。但看到網上分析文章偏少,如有分析錯誤望師傅們指出。 ### 概述 在這裡簡單來講講JRMP協議相關內容,JRMP是一個Java遠端方法協議,該協議基於TCP/IP之上,RMI協議之下。也就是說RMI該協議傳遞時底層使用的是JRMP協議,而JRMP底層則是基於TCP傳遞。 RMI預設使用的JRMP進行傳遞資料,並且JRMP協議只能作用於RMI協議。當然RMI支援的協議除了JRMP還有IIOP協議,而在Weblogic裡面的T3協議其實也是基於RMI去進行實現的。 RMI內容,具體參考:[Java安全之RMI協議分析](https://www.cnblogs.com/nice0e3/p/14280278.html) ## 0x01 JRMP模組利用 一、 ysoserial中的`exploit/JRMPClient`是作為攻擊方的程式碼,一般會結合`payloads/JRMPLIstener`使用。 攻擊流程如下: 1. 需要傳送`payloads/JRMPLIstener`內容到漏洞伺服器中,在該伺服器反序列化完成我們的payload後會開啟一個RMI的服務監聽在設定的埠上。 2. 我們還需要在我們自己的伺服器使用`exploit/JRMPClient`與存在漏洞的伺服器進行通訊,並且傳送一個gadgets物件,達到一個命令執行的效果。(前面說過RMI協議在傳輸都是傳遞序列化,接收資料後進行反序列化操作。) 簡單來說就是將一個payload傳送到伺服器,伺服器反序列化操作該payload過後會在指定的埠開啟RMI監聽,然後通過`exploit/JRMPClient` 去傳送攻擊 gadgets物件。 二、第二種利用方式和上面的類似`exploit/JRMPListener`作為攻擊方進行監聽,在反序列化漏洞位置傳送`payloads/JRMPClient`向我們的`exploit/JRMPListener`進行連線,連線後會返回在`exploit/JRMPListener`的gadgets物件並且進行反序列化 攻擊流程如下: 1. 攻擊方在自己的伺服器使用`exploit/JRMPListener`開啟一個rmi監聽 2. 往存在漏洞的伺服器傳送`payloads/JRMPClient`,payload中已經設定了攻擊者伺服器ip及JRMPListener監聽的埠,漏洞伺服器反序列化該payload後,會去連線攻擊者開啟的rmi監聽,在通訊過程中,攻擊者伺服器會發送一個可執行命令的payload(假如存在漏洞的伺服器中有使用`org.apacje.commons.collections`包,則可以傳送`CommonsCollections`系列的payload),從而達到命令執行的結果。 在前文中的 [Java 安全之Weblogic 2017-3248分析](https://www.cnblogs.com/nice0e3/p/14275298.html)文章中,用到的時候第二種方式進行繞過補丁。前文中並沒有對該模組去做分析,只是知道了利用方式和繞過方式,下面對JRMP模組去做一個深入的分析。檢視內部是如何實現該功能的。 ## 0x01 payloads/JRMPListener 該鏈的作用是在反序列化過後,在指定埠開啟一個JRMP Server。後面會配合到`exploit/JRMPClient`連線並且傳送payload。 ### 利用鏈 下面來看一下他的利用鏈 ```java /** * Gadget chain: * UnicastRemoteObject.readObject(ObjectInputStream) line: 235 * UnicastRemoteObject.reexport() line: 266 * UnicastRemoteObject.exportObject(Remote, int) line: 320 * UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383 * UnicastServerRef.exportObject(Remote, Object, boolean) line: 208 * LiveRef.exportObject(Target) line: 147 * TCPEndpoint.exportObject(Target) line: 411 * TCPTransport.exportObject(Target) line: 249 * TCPTransport.listen() line: 319 * * Requires: * - JavaSE * * Argument: * - Port number to open listener to */ ``` ### 構造分析 首先需要檢視一下yso裡面是如何生成gadget物件的。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195338294-1313898317.png) ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195346975-513798064.png) 可以直接定位到getObject方法中。 getObject方法中前面第一行程式碼獲取了外部傳入進來的埠,轉換成int型別。 這個比較簡單,主要內容在下面這段程式碼中。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195356613-1487387245.png) 使用`Reflections.createWithConstructor`方法傳入三個引數獲取到一個`UnicastRemoteObject`的例項物件。傳入的引數第一個是`ActivationGroupImpl.class`,第二個是`RemoteObject.class`,而第三個則是一個Object的陣列,陣列中裡面是`RemoteRef.class`,第四個是`UnicastServerRef`傳入了剛剛獲取的埠的一個例項物件。 第一個引數使用的是 ActivationGroupImpl 是因為在利用的時候,本身就是利用的 UnicastRemoteObject 的 readObject 函式,第二個引數需要滿足兩個條件: 1. 要為 UnicastRemoteObject 的父類 2. 不能在建立的過程中有其他什麼多餘的操作,滿足這兩個條件的兩個類是:RemoteObject、RemoteServer 最後具體是怎麼獲取到的`UnicastRemoteObject`例項物件,這裡需要除錯跟蹤一下。 ### UnicastServerRef分析 在此之前,先來看看`new UnicastServerRef(jrmpPort)`的內部實現。先跟蹤最裡層的方法。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195413797-1649541015.png) `UnicastServerRef`的構造方法,內部會去再new一個LiveRef物件並且傳入輸入進來的埠的引數。 選擇跟蹤。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195424156-1268331084.png) 內部是new了一個ObjID,繼續跟蹤。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195448015-1728806047.png) 裡面還會去new一個UID賦值給space成員變數,UID這裡自然都知道是啥意思,這裡就不跟了,而下面隨機獲取一個值賦值給objNum。 #### ObjID - `ObjID`用於標識匯出到RMI執行時的遠端物件。 匯出遠端物件時,將根據用於匯出的API來隱式或明確地分配一個物件識別符號。 - 構造方法: ```java ObjID() 生成唯一的物件識別符號。 ObjID(int objNum) 建立一個“眾所周知”的物件識別符號。 ``` 執行完成後返回到這一步。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195458611-416950976.png) 這裡呼叫了構造方法的過載方法。選擇跟蹤一下。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195508795-1673912286.png) 到了這一步,var1的引數自然不用解釋,而後面的則是傳入的埠。 裡面再一次呼叫過載方法,並且在傳遞的第二個引數呼叫了`TCPEndpoint.getLocalEndpoint`並且傳入埠進行獲取例項化物件。繼續跟蹤。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195517159-716434735.png) 內部呼叫`getLocalEndpoint`過載方法,跟蹤。 **`getLocalEndpoint`方法說明:** 獲取指定埠上本地地址空間的終結點。如果埠號為0,則返回共享的預設端點物件,其主機名和埠可能已確定,也可能尚未確定。 內部呼叫`localEndpoints.get`方法並且傳入var5,也就是TCPEndpoint的例項物件。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195532942-1920699185.png) ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195544515-1975059949.png) localEndpoints是一個map型別的類物件,這裡get方法獲取了var5,對應的value值,型別為LinkedList。這裡獲取到的是一個null。 執行到下一步 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195554254-1793874861.png) 呼叫resampleLocalHost方法獲取String的值,跟蹤檢視實現。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195604010-1178021883.png) ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195612757-2114957340.png) localHost的值是通過getHostnameProperty方法進行獲取的。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195624504-118117720.png) 執行完成後,返回到`sun.rmi.transport.tcp#TCPEndpoint`,執行到一下程式碼中。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195634117-1819712014.png) 這裡的程式碼比較容易理解,var為空,new一個TCPEndpoint物件,並且傳入var7,var0,var1,var2。引數值是ip,埠,null,null。將該物件新增到var6裡面。 後面則是對var3的物件進行賦值,ip和埠都賦值到var3的成員變數裡面去。 最後就是呼叫`localEndpoints.put(var5, var6);`講var5, var6儲存到`localEndpoints`中。 最後進行返回var3物件。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195643571-2053663093.png) 執行完成後,回到這裡 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195653462-1881237483.png) 繼續跟蹤,構造方法的過載方法。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195702841-1473725770.png) 這裡就沒啥好說的了,就是賦值。 最後返回到外面入口的地方 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195711742-1186363083.png) 呼叫了父類的構造方法 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195721647-697937788.png) 到了這裡其實就已經跟蹤完了。 ### yos利用鏈分析 返回到這一步跟蹤`Reflections.createWithConstructor`檢視內部實現。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195732751-1889349712.png) ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195740773-68583490.png) 簡化一下程式碼: ```java Constructor objCons = RemoteObject.class.getDeclaredConstructor(new UnicastServerRef(jrmpPort)); ``` 其實也就是反射呼叫獲取 RemoteObject引數為UnicastRef的構造方法。並且傳遞`new UnicastServerRef(jrmpPort)`例項化物件作為構造方法引數。 而下面的`setAccessible(objCons);`這個就不做分析了,分析過前面的利用鏈都大概清楚,這個其實就是修改暴力反射的一個方法類。 看到下面這段程式碼 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195750068-1090585509.png) 這裡進行跟蹤。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195800351-1087457768.png) 其實藉助`ReflectionFactory.getReflectionFactory()`工廠方法在這裡就是返回了ReflectionFactory的例項物件。 跟蹤`newConstructorForSerialization`方法 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195809855-717143479.png) 這裡傳遞的var1 引數是`ActivationGroupImpl.class`物件,而var2是剛剛反射獲取的`Constructor`物件。 下面是個三目運算,如果`var2.getDeclaringClass() == var1`的話,返回var2,如果不低於的話,呼叫`this.generateConstructor(var1, var2);`後的執行結果進行返回。 將程式碼簡單化: ```java ActivationGroupImpl.class.getDeclaringClass()==ActivationGroupImpl.class ? var2 :this.generateConstructor(ActivationGroupImpl.class, var2) ``` 這裡呼叫了`this.generateConstructor`方法並且傳入了兩個引數。後來才發現後面的這些內容是屬於反射的底層實現,跟蹤跑偏了。感興趣的師傅們可以自行檢視。 ```java Constructor sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons); ``` 返回到這段程式碼,後來的查詢資料發現`newConstructorForSerialization`這個方法返回的是一個無參的constructor物件,但是絕對不會與原來的constructor衝突,被稱為munged 建構函式 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195827699-1672111365.png) 這裡先來思考到一個問題,為什麼不能使用反射直接呼叫呢? 其實並非所有的java類都有無參構造方法的,並且有的類的構造方法還是private的。所以這裡採用這種方式進行獲取。 再來看到上面的程式碼: ```java Constructor sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons); ``` 前面引數為`ActivationGroupImpl.class`,指定獲取`ActivationGroupImpl.class`的 Constructor。後面的引數為反射獲取RemoteObject的RemoteRef型別構造方法獲取到的Constructor類。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195837841-338557371.png) 最後將引數傳遞進行,返回建立一個`ActivationGroupImpl`例項化物件。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195847249-1317994549.png) 執行完成回到這個方法內,發現該地方對`ActivationGroupImpl`進行了向上轉型為`UnicastRemoteObject`型別 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195857084-1440450308.png) 最後呼叫反射將`UnicastRemoteObject`的例項物件的port欄位修改成我們設定的埠的值。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195903792-248951910.png) ## 0x02 除錯分析 test類: ```java package ysoserial.test; import ysoserial.payloads.JRMPClient; import ysoserial.payloads.JRMPListener; import java.io.*; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; public class test { public static void main(String[] args) throws Exception { JRMPListener jrmpListener = new JRMPListener(); UnicastRemoteObject object = jrmpListener.getObject("9999"); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream bjos = new ObjectOutputStream(bos); bjos.writeObject(object); ByteArrayInputStream bait = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ojis = new ObjectInputStream(bait); Object o = ojis.readObject(); } } ``` 這裡是利用了`UnicastRemoteObject`的`readObject`作為反序列化的入口點。 在此處下斷點開始除錯分析。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195915125-699242982.png) `readObject`方法處呼叫了`reexport`方法,跟蹤檢視。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195923824-1378179952.png) csf和ssf為空,執行到這裡。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195933237-265483822.png) 呼叫`exportObject `方法並且傳入this和port 這裡的this,實際上是`ActivationGroupImpl`,因為前面進行了向上轉型。跟蹤`exportObject `。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195944625-969366660.png) 這裡再次呼叫過載方法,跟蹤檢視。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116195957729-2006704461.png) 到了這一步,呼叫`sref.exportObject`傳入前面建立的例項物件。跟蹤。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116200008027-1964872614.png) 這裡下面呼叫this.ref,而this.ref為LiveRef物件。這一段則是呼叫`LiveRef.exportObject`。繼續跟蹤。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116200017166-1766239139.png) this.ep為Endpoint物件,這裡呼叫的是`Endpoint.exportObject`,這裡的物件是怎麼賦值的前面的構造分析的時候去講過,這裡不做多的贅述。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116200027566-307529688.png) 呼叫`this.transport.exportObject;`繼續跟蹤。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116200041868-1641283458.png) 到了這一步就呼叫了`this.listen()`進行啟動監聽。 ![](https://img2020.cnblogs.com/blog/1993669/202101/1993669-20210116200050619-688581880.png) ### 參考文章 [ysoserial JRMP相關模組分析(一)- payloads/JRMPListener](https://xz.aliyun.com/t/2649#toc-0) ## 0x03 結尾 JRMP的這個模組第一次分析還是挺費勁的,網上的相關資料也