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

nice_0e3發表於2021-01-27

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

首發安全客:Java安全之ysoserial-JRMP模組分析(一)

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協議分析

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分析文章中,用到的時候第二種方式進行繞過補丁。前文中並沒有對該模組去做分析,只是知道了利用方式和繞過方式,下面對JRMP模組去做一個深入的分析。檢視內部是如何實現該功能的。

0x01 payloads/JRMPListener

該鏈的作用是在反序列化過後,在指定埠開啟一個JRMP Server。後面會配合到exploit/JRMPClient連線並且傳送payload。

利用鏈

下面來看一下他的利用鏈

/**
 * 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物件的。

可以直接定位到getObject方法中。

getObject方法中前面第一行程式碼獲取了外部傳入進來的埠,轉換成int型別。

這個比較簡單,主要內容在下面這段程式碼中。

使用Reflections.createWithConstructor方法傳入三個引數獲取到一個UnicastRemoteObject的例項物件。傳入的引數第一個是ActivationGroupImpl.class,第二個是RemoteObject.class,而第三個則是一個Object的陣列,陣列中裡面是RemoteRef.class,第四個是UnicastServerRef傳入了剛剛獲取的埠的一個例項物件。

第一個引數使用的是 ActivationGroupImpl 是因為在利用的時候,本身就是利用的 UnicastRemoteObject 的 readObject 函式,第二個引數需要滿足兩個條件:

  1. 要為 UnicastRemoteObject 的父類

  2. 不能在建立的過程中有其他什麼多餘的操作,滿足這兩個條件的兩個類是:RemoteObject、RemoteServer

最後具體是怎麼獲取到的UnicastRemoteObject例項物件,這裡需要除錯跟蹤一下。

UnicastServerRef分析

在此之前,先來看看new UnicastServerRef(jrmpPort)的內部實現。先跟蹤最裡層的方法。

UnicastServerRef的構造方法,內部會去再new一個LiveRef物件並且傳入輸入進來的埠的引數。

選擇跟蹤。

內部是new了一個ObjID,繼續跟蹤。

裡面還會去new一個UID賦值給space成員變數,UID這裡自然都知道是啥意思,這裡就不跟了,而下面隨機獲取一個值賦值給objNum。

ObjID

  • ObjID用於標識匯出到RMI執行時的遠端物件。 匯出遠端物件時,將根據用於匯出的API來隱式或明確地分配一個物件識別符號。

  • 構造方法:

    ObjID() 
    生成唯一的物件識別符號。 
    ObjID(int objNum) 
    建立一個“眾所周知”的物件識別符號。 
    

執行完成後返回到這一步。

這裡呼叫了構造方法的過載方法。選擇跟蹤一下。

到了這一步,var1的引數自然不用解釋,而後面的則是傳入的埠。

裡面再一次呼叫過載方法,並且在傳遞的第二個引數呼叫了TCPEndpoint.getLocalEndpoint並且傳入埠進行獲取例項化物件。繼續跟蹤。

內部呼叫getLocalEndpoint過載方法,跟蹤。

getLocalEndpoint方法說明:

獲取指定埠上本地地址空間的終結點。如果埠號為0,則返回共享的預設端點物件,其主機名和埠可能已確定,也可能尚未確定。

內部呼叫localEndpoints.get方法並且傳入var5,也就是TCPEndpoint的例項物件。

localEndpoints是一個map型別的類物件,這裡get方法獲取了var5,對應的value值,型別為LinkedList。這裡獲取到的是一個null。

執行到下一步

呼叫resampleLocalHost方法獲取String的值,跟蹤檢視實現。

localHost的值是通過getHostnameProperty方法進行獲取的。

執行完成後,返回到sun.rmi.transport.tcp#TCPEndpoint,執行到一下程式碼中。

這裡的程式碼比較容易理解,var為空,new一個TCPEndpoint物件,並且傳入var7,var0,var1,var2。引數值是ip,埠,null,null。將該物件新增到var6裡面。

後面則是對var3的物件進行賦值,ip和埠都賦值到var3的成員變數裡面去。

最後就是呼叫localEndpoints.put(var5, var6);講var5, var6儲存到localEndpoints中。

最後進行返回var3物件。

執行完成後,回到這裡

繼續跟蹤,構造方法的過載方法。

這裡就沒啥好說的了,就是賦值。

最後返回到外面入口的地方

呼叫了父類的構造方法

到了這裡其實就已經跟蹤完了。

yos利用鏈分析

返回到這一步跟蹤Reflections.createWithConstructor檢視內部實現。

簡化一下程式碼:

Constructor<? super T> objCons = RemoteObject.class.getDeclaredConstructor(new UnicastServerRef(jrmpPort));

其實也就是反射呼叫獲取 RemoteObject引數為UnicastRef的構造方法。並且傳遞new UnicastServerRef(jrmpPort)例項化物件作為構造方法引數。

而下面的setAccessible(objCons);這個就不做分析了,分析過前面的利用鏈都大概清楚,這個其實就是修改暴力反射的一個方法類。

看到下面這段程式碼

這裡進行跟蹤。

其實藉助ReflectionFactory.getReflectionFactory()工廠方法在這裡就是返回了ReflectionFactory的例項物件。

跟蹤newConstructorForSerialization方法

這裡傳遞的var1 引數是ActivationGroupImpl.class物件,而var2是剛剛反射獲取的Constructor物件。

下面是個三目運算,如果var2.getDeclaringClass() == var1的話,返回var2,如果不低於的話,呼叫this.generateConstructor(var1, var2);後的執行結果進行返回。

將程式碼簡單化:

ActivationGroupImpl.class.getDeclaringClass()==ActivationGroupImpl.class ? var2
   :this.generateConstructor(ActivationGroupImpl.class, var2)

這裡呼叫了this.generateConstructor方法並且傳入了兩個引數。後來才發現後面的這些內容是屬於反射的底層實現,跟蹤跑偏了。感興趣的師傅們可以自行檢視。

Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);

返回到這段程式碼,後來的查詢資料發現newConstructorForSerialization這個方法返回的是一個無參的constructor物件,但是絕對不會與原來的constructor衝突,被稱為munged 建構函式

這裡先來思考到一個問題,為什麼不能使用反射直接呼叫呢?

其實並非所有的java類都有無參構造方法的,並且有的類的構造方法還是private的。所以這裡採用這種方式進行獲取。

再來看到上面的程式碼:

Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);

前面引數為ActivationGroupImpl.class,指定獲取ActivationGroupImpl.class的 Constructor。後面的引數為反射獲取RemoteObject的RemoteRef型別構造方法獲取到的Constructor類。

最後將引數傳遞進行,返回建立一個ActivationGroupImpl例項化物件。

執行完成回到這個方法內,發現該地方對ActivationGroupImpl進行了向上轉型為UnicastRemoteObject型別

最後呼叫反射將UnicastRemoteObject的例項物件的port欄位修改成我們設定的埠的值。

0x02 除錯分析

test類:

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();

    }
}

這裡是利用了UnicastRemoteObjectreadObject作為反序列化的入口點。

在此處下斷點開始除錯分析。

readObject方法處呼叫了reexport方法,跟蹤檢視。

csf和ssf為空,執行到這裡。

呼叫exportObject 方法並且傳入this和port 這裡的this,實際上是ActivationGroupImpl,因為前面進行了向上轉型。跟蹤exportObject

這裡再次呼叫過載方法,跟蹤檢視。

到了這一步,呼叫sref.exportObject傳入前面建立的例項物件。跟蹤。

這裡下面呼叫this.ref,而this.ref為LiveRef物件。這一段則是呼叫LiveRef.exportObject。繼續跟蹤。

this.ep為Endpoint物件,這裡呼叫的是Endpoint.exportObject,這裡的物件是怎麼賦值的前面的構造分析的時候去講過,這裡不做多的贅述。

呼叫this.transport.exportObject;繼續跟蹤。

到了這一步就呼叫了this.listen()進行啟動監聽。

參考文章

ysoserial JRMP相關模組分析(一)- payloads/JRMPListener

0x03 結尾

JRMP的這個模組第一次分析還是挺費勁的,網上的相關資料也偏少。

相關文章