隱祕的角落 -- JDK CORBA 安全性研究(上)

作者:螞蟻安全非攻實驗室
公衆號:螞蟻安全實驗室

特別推薦

諸葛建偉 清華大學網絡科學與網絡空間研究院副研究員

“對非可信數據的反序列化”一直是應用安全領域中常見且高危的安全漏洞類型,在各種不同Web開發語言、分佈式架構及中間件框架中大面積流行,也經常被攻擊者發掘並利用形成對業務應用的遠程代碼執行突破口。網絡安全研究領域對反序列化漏洞已有較多研究和技術文章的發表,甚至學術界也曾對此問題進行過關注與研究。

而正如文章標題所指,螞蟻安全實驗室的這篇分享揭密了反序列化安全風險的一個“隱祕角落”:Java開發分佈式應用中流行使用的COBRA架構,這個角落直到2019年才被研究者公開涉足。本文以由淺入深,案例驅動的方式詳細介紹了COBRA基本架構及其應用細節,並全方位地從不同角度分析了COBRA架構所面臨的反序列化漏洞風險,技術內容的詳盡程度與行文風格對Web安全的研究人員和技術愛好者都非常友好,對理解Java COBRA架構的最新安全風險有很大的幫助。

一、背景

在移動互聯網時代,互聯網平臺爲了服務海量用戶和支持高併發業務場景,服務端分佈式架構已經成爲了主流的應用部署架構。CORBA、JAVA RMI、DCOM等分佈式技術先後誕生且得到了廣泛應用,其安全性也成爲影響互聯網生態安全的重要因素。以CORBA爲例,目前其協議仍然被很多JAVA中間件、基礎設施支持,例如weblogic、websphere、glassfish等,研究其協議實現的安全性,對於互聯網基礎設施安全防護有着重要價值。

在JAVA分佈式架構中存在着大量的序列化與反序列化操作令人擔心其安全風險,但並非所有的反序列化框架都存在安全風險,爲此於今年我們提出了開放動態反序列化(ODD,Open Dynamic Deserialization)的概念以揭示反序列化中真正的安全風險。ODD簡單來說就是應用架構支持在反序列化過程中動態生成任意類型的對象。ODD的核心是“開放”和“動態”,是爲了提升應用開發的靈活性和效率而設計。但是從安全角度來說,“開放”和“動態”本質上是不安全的,它容易失去對程序行爲的控制,導致非安全輸入對程序行爲的任意劫持,從而形成一個集中的RCE(遠程代碼執行)突破點。

ODD這種漏洞本質雖然是我們在今年的fastjson應急中總結並明確的,但這種漏洞類型在歷史上已經引起了大量的安全問題,各類分佈式技術以及系統均受到了非常大的挑戰。2015年Gabriel Lawrence和Chris Frohoff在AppSecCali上發表的著名安全報告"Marshalling Pickles",提出了POP(Property-Oriented Programing)攻擊鏈,能夠利用JAVA體系中ODD設計導致的安全缺陷實現RCE,ODD類型反序列化漏洞在JAVA領域影響面被急劇擴大。在報告中,作者也明確警告 Avoid magic -- Avoid open-ended (de)serialization when possible,即不要做開放式反序列化。但顯然業界並沒有把這個警告當回事,ODD安全漏洞愈演愈烈,首當其衝的就是 JAVA RMI 及其相關應用系統。@pwntester在2016年black hat黑客大會中提出了針對JAVA RMI技術的一系列攻擊方式,除在當時的安全研究圈引起巨大轟動以外,其攻擊思路至今仍然被各red team引用並作爲其主要武器之一。

過去幾年,行業中針對CORBA安全性的公開分享並不多。直到2019年,@An Trinh在當年的blackhat黑客大會上提出了針對 IIOP 協議的反序列化攻擊方式,而 IIOP正是用來在CORBA對象請求代理之間交流的協議。此後RMI-IIOP相關的漏洞井噴式爆發, 2020年相關 CVE數量多達20+且基本都能造成 RCE,例如經典的 CVE-2020-4450CVE-2020-2551

我們的JAVA安全研究工作很早就已經覆蓋CORBA,出於“拋磚引玉”的想法,我們把研究過程中積累的思路和經驗形成兩篇文章分享出來:

· 隱祕的角落--JDK CORBA 安全性研究(上):介紹CORBA基本架構以及淺析實現細節,爲後續安全風險分析打基礎。

· 隱祕的角落--JDK CORBA 安全性研究(下):從客戶端、服務端和通信協議三部分,全方位分析CORBA安全風險,並討論如何防範。

二、基礎概念

什麼是 CORBA?

CORBA 從概念上擴展了 RPC,它是一種面向對象的 RPC,RPC 應用都是面向過程的,而 CORBA 應用是面向對象的。

那麼什麼是RPC?

RPC(Remote Promote Call) 遠程過程調用協議。RPC使得程序能夠像訪問本地系統資源一樣,去訪問遠端系統資源。

簡單的說,RPC就是從一臺機器(客戶端)上通過參數傳遞的方式調用另一臺機器(服務器)上的一個函數或方法(可以統稱爲服務)並得到返回的結果。

CORBA 流程設計如下:

CORBA 體系如下:

(靜態存框->靜態存根)

客戶端調用靜態存根(static stubs)向服務器發出請求,存根(stubs)是代理對象支持的客戶端程序。

服務器端調用靜態框架(static skeleton)處理客戶端請求,框架(skeleton)是服務器端程序。

一些基礎術語,如下(可跳過,在詳細閱讀後文過程中再查看):

IOR:可互操作對象引用,類似 JDBC 數據庫連接信息或者 JNDI 連接信息對象等,用於傳輸對象之間的操作信息。

ORB(Object Request Broker):對象請求代理。ORB 是一箇中間件,他在對象間建立客戶-服務器的關係。通過 ORB,一個客戶可以很簡單地使用服務器對象的方法。ORB 截獲客戶端的方法調用,然後負責找到服務端方法實現並且傳遞參數,最後將返回方法執行結果。客戶不用知道對象在哪裏,是什麼語言實現的。

ORBD(ORB守護程序):負責查找 IOR 指定的對象實現,以及建立客戶機和服務器之間的連接。一旦建立了連接,GIOP 將定義一組由客戶機用於請求或服務器用於響應的消息。

GIOP(General Inter-ORB Protocol):GIOP 元件提供了一個標準傳輸語法(低層數據表示)和ORB之間通信的信息格式集。GIOP只能用在ORB與ORB之間,而且,只能在符合理想條件的面向連接傳輸協議中使用。

IIOP(Internet Inter-ORB Protocol):IIOP 是 CORBA 的通信協議,用於CORBA對象RPC請求之間的交流。

IDL:IDL全稱接口定義語言,是用來描述軟件組件接口的一種規範語言。用戶可以定義模塊、接口、屬性、方法、輸入輸出參數。Java 中提供了 idlj 命令用來編譯 IDL 描述文件,用以生成 Java 語言的 客戶端 java 文件等。

CORBA與ORB的關係:CORBA的分佈式對象調用能力依賴於ORB,而ORB之間進行通信是通過GIOP協議完成的。GIOP定義了ORB之間互操作的傳輸語法和標準消息格式,比如請求頭、請求體所包含的字段和長度。

IIOP與GIOP的關係 :IIOP與GIOP的關係就象特殊語言與OMG IDL之間的關係;GIOP能被映射到不同層,它能指定協議。就象IDL不能見着完整的程序一樣,GIOP 本身也不能提供完整的協作工作。IIOP和不同傳輸層上的其它相似映射,實現抽象的GIOP定義。GIOP是一個抽象的協議,而IIOP是其一個具體的實現,定義瞭如何通過TCP/IP協議交換GIOP消息。

三、環境準備

首先,嘗試構建一個簡單的 corba 應用

這裏已經準備好了一套JDK CORBA 環境,git clone 後直接使用 idea 打開即可,代碼都是在 JDK 8u221 環境中運行過。

四、idl 簡單編寫以及idlj 使用

首先編寫一個簡單的 hello.idl,如下:

module com {

  interface Hello{

     string sayHello();

   };

};

如上,module 名在 java 源碼中表示爲 package,設置一個接口類 Hello,類中含有一個無參、返回類型爲 String 的 sayHello 函數。

然後使用 JDK 自帶的 idlj 工具生成 client 和 server 代碼,命令如下:

idlj -fall hello.idl

注:idlj -fall hello.idl 可以生成 server 、client 端所需的所有 class,如果只需要 client 端或 server 端的話,使用 -fclient / -fserver 即可。

命令執行完成後,會直接在當前目錄下生成 com 目錄,目錄中含有 6 個文件如下:

五、本地嘗試

爲了方便觀察,我將 server 運行在本地。

首先啓動 ORBD 服務器,運行如下命令,會監聽本地 1050 和 1049 端口:

orbd -port 1050 -ORBInitialPort 1049 -ORBInitialHost localhost

隨後運行 HelloServer,效果如下:

最後運行 HelloClient,效果如下:

如上圖,已經調用成功了,接下來簡單分析一下整個通信流程。

六、通信過程

經過簡單的抓包分析,得出整個通信過程如下圖:

如上圖:

首先會啓動 ORBD 作爲 name service 的服務器,會創造 name service 服務。

第二步,corba server 端向 orbd 獲取 name service,協商好通信格式。

第三步,orbd 返回自己保存的 name service。

第四步,corba server 端拿到 name service 後,會將自己的 corba 服務綁定到 name service 上面(流程和 rmi 類似)。

第五步,corba client 端這個時候想要查找 corba server 提供的某個服務,先向 orbd 發起請求,獲取 name service。

第六步,orbd 來者不拒,將自己保存的 name service 返回給 client 端。

第七步,corba client 端利用 name service 查找到某個 corba server 端提供的服務(client 端獲得的是 stub),然後發起一個 rpc 請求,要求 corba server 響應。

第八步,corba server 在監聽到 corba client 端的請求後,一頓調用並且計算出結果,然後將其打包封裝,最後返回給 corba client。

以上,就是一個 corba 應用的一次遠程調用的通信流程。

使用 wireshark 抓取通信流量,如下圖:

七、Client 解析

Client 端主要是通過 stub 遠程調用 Server 端。

stub 類是 client 端調用 orb 的媒介,stub 、orb 關係,借用一張圖表述如下:

client 通過對 stub 的調用,間接調用了 server 端的函數實現。

stub 會對客戶端的調用參數和調用請求進行封裝交給 orb,而後 orb 通過調用分派機制與 server 端通信,server端獲取到了cliant端的調用請求,將請求參數帶入請求操作(調用函數)中,最終返回給 orb 一個 response,orb 傳遞給 client 的 stub ,stub 傳遞給 client 調用者,簡單流程如下:

客戶端含有 Hello 、_HelloStub.... 服務端含有 HelloImpl

#1 client 發起調用:sayHello()

->

#2 stub 封裝 client 的調用請求,併發送給 orbd

->

#3 orb 接受請求,根據 server 端註冊信息,分派給 server 端處理調用請求

->

#4 server 接受調用請求,執行 sayHello ,並將執行結果進行封裝,傳遞給 orbd

->

#5 ordb 收到 server 端的返回後,將其傳遞給 stub

->

#6 stub 收到請求後,解析返回二進制流,提取 server 端的處理結果

->

#7 最終結果會返回給 client 調用者

八、stub的生成

stub 類是存在於 client 端的 server 端的 handle。生成方法有好幾種,在此只列舉三種,如下:

· 1. 使用代碼先獲取 NameServer ,然後 resolve_str

· 2. 使用 ORB.string_to_object

· 3. 使用 javax.naming.InitialContext.lookup

1. 通過 NameServer 獲取

示例代碼如下:

Properties props = new Properties();

// 生成一個ORB,並初始化,這個和Server端一樣

props .put("org.omg.CORBA.ORBInitialPort", "1050");

props.put("org.omg.CORBA.ORBInitialHost", "192.168.0.2");

ORB orb = ORB.init(args, props);

// 獲得根命名上下文

org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");

// 用NamingContextExt代替NamingContext.

NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);

// 通過名稱獲取服務器端的對象引用

String name = "Hello";

Hello hello = HelloHelper.narrow(ncRef.resolve_str(name));

2. 通過 ORB.string_to_object

示例代碼如下:

ORB orb = ORB.init(args, null);
org.omg.CORBA.Object obj 
= orb.string_to_object("corbaname::192.168.0.2:1050#Hello");
Hello hello = HelloHelper.narrow(obj);

如上代碼,傳入的參數是 corbaname: 開頭的字符串,string_to_object 支持三種協議:

corbaname: 、 corbaloc: 、 IOR:

2.1 IOR

IOR 是一個數據結構,它提供了關於類型、協議支持和可用 ORB 服務的信息。ORB 創建、使用並維護該 IOR。

簡單可以理解爲,存儲着 corba server 相關 rpc 信息,以 IOR:XXX 形式表現的字符串,如:

IOR:000000000000000100000000000000010000000000000027000100000000000b33302e35322e38382e370000041a00000000000b4e616d6553657276696365

2.2 corbaloc

corbaloc 經過處理最終也是生成一個 IOR ,然後通過 IOR 創建出一個 stub

2.3 corbaname

他的處理邏輯如下:

如上圖,這完全和第一種通過 NameServer 獲取 stub 的方式一樣,後續的調用鏈在 client 安全風險分析過程中會展示出來。

3.通過 jndi (javax.naming.InitialContext.lookup)

代碼如下:

ORB orb = ORB.init(args, null);

Hashtable env = new Hashtable(5, 0.75f);

env.put("java.naming.corba.orb", orb);

Context ic = new InitialContext(env);

// resolve the Object Reference using JNDI

Hello helloRef 
=HelloHelper.narrow((org.omg.CORBA.Object)ic.lookup("corbaname::192.168.0.2:1050#Hello"));

如上述代碼,也是使用的 corbaname 作爲協議開頭,因爲 jndi 同時支持3中寫法:

iiopname:

iiop:

corbaname:

其中, iiopname 和 iiop 開頭的協議串,最終會轉換成 corbaloc 開頭的協議串。corbaname 開頭的協議,會觸發 org.omg.CosNaming._NamingContextStub#resolve 調用。

resolve 函數 和 resolve_str 函數實現邏輯是一樣的、執行結果也相同,只是參數類型不同而已。

九、client 端調用 rpc

使用方式目前只收集到 2 種:

· 1. 通過 client 端 stub 進行調用

· 2. 通過 Dynamic Invocation Interface(dii request)調用

1. stub 調用

代碼如下:

Properties props = new Properties();

// 生成一個ORB,並初始化,這個和Server端一樣

props .put("org.omg.CORBA.ORBInitialPort", "1050");

props.put("org.omg.CORBA.ORBInitialHost", "192.168.0.2");

ORB orb = ORB.init(args, props);

// 獲得根命名上下文

org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");

// 用NamingContextExt代替NamingContext.

NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);

// 通過名稱獲取服務器端的對象引用

String name = "Hello";

Hello hello = HelloHelper.narrow(ncRef.resolve_str(name));

//調用遠程對象

System.out.println(hello.sayHello());

2. dii 調用

代碼如下:

Properties props = new Properties();

// 生成一個ORB,並初始化,這個和Server端一樣

props .put("org.omg.CORBA.ORBInitialPort", "1050");

props.put("org.omg.CORBA.ORBInitialHost", "192.168.0.2");

ORB orb = ORB.init(args, props);

// 獲得根命名上下文

org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");

// 用NamingContextExt代替NamingContext.

NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);

// 通過名稱獲取服務器端的對象引用

String name = "Hello";

Hello hello = HelloHelper.narrow(ncRef.resolve_str(name));

Request request = hello._request("sayHello");

request.invoke();

System.out.println(request.result().value());

如上述代碼,在 stub 獲取的部分和 stub 調用方式完全一樣,後續是通過獲取 com.sun.corba.se.impl.corba.RequestImpl 以此來進行 dii 調用的(Dynamic Invocation Interface)。

十、Server 解析

服務註冊

回顧一下 HelloServer 中註冊服務的代碼,如下:

// 獲得命名上下文 NameService

org.omg.CORBA.Object objref = orb.resolve_initial_references("NameService");

// 使用NamingContextExt 它是 INS(Interoperable Naming Service,協同命名規範)的一部分

NamingContextExt ncRef = NamingContextExtHelper.narrow(objref);

// 綁定一個對象引用,以便客戶端可以調用

String name = "Hello";

NameComponent[] nc = ncRef.to_name(name);

ncRef.rebind(nc, href);

如上代碼,在獲取到 NameService 後隨即開始註冊服務,服務名叫做 Hello,client 端可以通過服務名在 NameService 中搜索服務。在此調用 NamingContextExt#rebind 是向 ORBD 發送一個重綁定請求。

派遣請求

在服務綁定完成後,服務端會開始監聽一個高端口等待客戶端的連接通信。

下圖是客戶端發起請求後,服務端派遣請求的工作流程:

至此,JDK CORBA 基本概念介紹結束。

十一、安全風險

經過分析和探索,發現了 client 端、server 端、orbd 端含有如下風險點:

· client 端 ,存在反序列化風險和遠程類加載風險

· server 端,存在反序列化風險

· orbd,存在反序列化風險

在下篇中,將會分析 JDK CORBA 中存在的風險點。

參考文獻

RPC基本原理:https://www.cnblogs.com/sumuncle/p/11554904.html

CORBA Website:https://www.corba.org/

wiki:https://en.wikipedia.org/wiki/Common_Object_Request_Broker_Architecture

基本概念:https://www.cnblogs.com/zhuchunling/p/9540541.html

構建簡單的 corba 應用:https://blog.csdn.net/chjttony/article/details/6561466

corba 簡介:https://blog.csdn.net/chjttony/article/details/6543116

corba 通信過程淺析:http://weinan.io/2017/05/03/corba-iiop.html

關於作者

螞蟻安全非攻實驗室:隸屬於螞蟻安全九大實驗室之一。螞蟻安全非攻實驗室致力於JAVA安全技術研究,覆蓋螞蟻自研框架和中間件、經濟體開源產品以及行業中廣泛使用的第三方開源產品,通過結合程序自動化分析技術和AI技術,深度挖掘相關應用的安全風險,構建可信的安全架構解決方案。


Paper 本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址:https://paper.seebug.org/1445/

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