Java反序列化漏洞從理解到實踐

2021-02-19 FreeBuf

一、前言

在學習新事物時,我們需要不斷提醒自己一點:紙上得來終覺淺,絕知此事要躬行。這也是為什麼我們在學到知識後要付諸實踐的原因所在。在本文中,我們會深入分析大家非常熟悉的Java發序列化漏洞。對我們而言,最好的實踐就是真正理解手頭掌握的知識,並可以根據實際需要加以改進利用。本文的主要內容包括以下兩方面:

1. 利用某個反序列化漏洞。

2. 自己手動創建利用載荷。

更具體一點,首先我們會利用現有工具來實際操作反序列化漏洞,也會解釋操作的具體含義,其次我們會深入分析載荷相關內容,比如什麼是載荷、如何手動構造載荷等。完成這些步驟後,我們就能充分理解載荷的工作原理,未來碰到類似漏洞時也能掌握漏洞的處理方法。

整個過程中需要用到的工具都會在本文給出,但我建議你先了解一下這個工具:

https://github.com/NickstaDB/DeserLab

該工具包含我們準備實踐的漏洞。之所以選擇使用模擬漏洞而不是實際目標,原因在於我們可以從各個方面控制這個漏洞,因此也可以更好理解反序列化漏洞利用的工作原理。

二、利用DeserLab漏洞

首先你可以先讀一下Nick寫的這篇文章, 文章中介紹了DeserLab以及Java反序列化相關內容。本文會詳細介紹Java序列化協議的具體細節。閱讀完本文後,你應該可以自己搞定DeserLab環境。接下來我們需要使用各種預編譯jar工具,所以我們可以先從Github上下載這些工具。現在準備步入正題吧。

碰到某個問題後,我通常的做法是先了解目標的正常工作方式。對於DeserLab來說,我們需要做以下幾件事情:

運行伺服器及客戶端

抓取通信流量

理解通信流量

我們可以使用如下命令來運行伺服器及客戶端:

上述命令的運行結果如下:

上述結果並不是我們想要的信息,我們想問的問題是,這個環境如何實現反序列化功能?為了回答這個問題,我們可以使用wireshark、tcpdump或者tshark來捕捉6666埠上的流量。我們可以使用如下命令,利用tcpdump來捕捉流量:

在繼續閱讀本文之前,你可以先用wireshark來瀏覽一下pcap文件。讀完Nick的文章後,你應該已經了解目前所處的狀況,至少能夠識別出隱藏在流量中的序列化Java對象。


2.1 提取序列化數據

根據這些流量,我們可以肯定的是網絡中有序列化數據正在傳輸,現在讓我們來分析哪些數據正在傳輸。我選擇使用SerializationDumper工具來解析這些流量,這個工具屬於我們要用的工具集之一,作用與jdeserialize類似,後者屬於聞名已久且尚能發揮作用的老工具。在使用這些工具之前,我們需要先準備好待處理數據,因此,我們需要將pcap轉換為可待分析的數據格式。

這條命令雖然看起來很長,但至少能正常工作。我們可以將這條命令分解為更好理解的子命令,因為該命令的功能是將pcap數據轉換為經過十六進位編碼的一行輸出字符串。首先,該命令將pcap轉換為文本,文本中只包含傳輸的數據、TCP源埠號以及目的埠號:

結果如下所示:

如上述結果所示,在TCP三次握手期間並沒有傳輸數據,因此你可以看到』,,』這樣一段文本。隨後,客戶端發送第一個字節,伺服器返回ACK報文,然後再發回某些字節數據,以此類推。命令的第二個功能是繼續處理這些文本,根據埠以及每一行的開頭部分來選擇輸出合適的載荷:

這條過濾命令會將伺服器的響應數據提取出來,如果你想要提取客戶端數據,你需要改變埠號。處理結果如下所示:

這些數據正是我們需要的數據,它將發送和接收數據以較為簡潔的方式表示出來。我們可以使用前面提到的兩個工具來處理這段數據,首先我們使用的是SerializationDumper,然後我們會再使用jdeserialize。之所以要這麼做,原因在於使用多個工具來處理同一個任務可以便於我們分析潛在的錯誤或問題。如果你堅持使用一個工具的話,你可能會不小心走進錯誤的死胡同。當然嘗試不同的工具本身就是一件非常有趣的事情。

2.2 分析序列化數據

SerializationDumper工具的使用非常簡單直白,我們只需要將十六進位形式的序列化數據作為第一個參數傳輸進去即可,如下所示:

結果如下所示:

我們需要編譯才能使用jdeserialize工具。編譯任務可以使用ant)以及build.xml文件來完成,我選擇手動編譯方式,具體命令如下:

上述命令可以生成jar文件,你可以使用如下命令輸出幫助信息以測試jar文件是否已正確生成:

jdeserialize工具需要一個輸入文件,因此我們可以使用python之類的工具將十六進位的序列化數據保存成文件,如下所示(我縮減了十六進位字符串以便閱讀):

接下來,我們使用待處理文件名作為第一個參數,傳遞給jdeserialize工具,處理結果如下所示:

從這兩個分析工具的輸出中,我們首先可以確認的是,這段數據的確是序列化數據。其次,我們可以確認的是,客戶端和伺服器之間正在傳輸一個「nb.deser.HashRequest」對象。結合工具的輸出結果以及前面的wireshark抓包數據,我們可知用戶名以字符串形式存儲在TC_BLOCKDATA類型中進行傳輸:

現在我們對DeserLab客戶端與伺服器之間的通信過程已經非常熟悉,接下來我們可以使用ysoserial工具來利用這個過程。

2.3 利用DeserLab中的漏洞

根據pcap的分析結果以及序列化數據的分析結果,我們已經非常熟悉整個環境的通信過程,因此我們可以構建自己的python腳本,腳本中可以嵌入ysoserial載荷。為了保持代碼的簡潔,也為了匹配wireshark數據流,我決定使用類似wireshark數據流的方式來實現這段代碼,如下所示:

你可以在這裡找到完整版的代碼。 如你所見,最簡單的方法是將所有java反序列化交換數據硬編碼到代碼中。你可能對代碼的具體寫法有些疑問,比如為什麼mydeser.exploit(myargs.payloadfile)位於mydeser.clientname()之後,以及我根據什麼來決定代碼的具體位置。因此我想解釋一下我的思考過程,也順便介紹一下如何生成並發送ysoserial載荷。

在讀完有關Java反序列化的幾篇文章之後(見本文的參考資料),我總結了兩點思想:

1、大多數漏洞都與Java對象的反序列化有關;

2、大多數漏洞都與Java對象的反序列化有關。

開個玩笑而已。所以如果我們檢查伺服器與客戶端的信息交互過程,我們可以在某個地方找到Java對象的交換過程。我們很容易就能在序列化數據的分析結果中找到這個目標,因為它要麼包含「TC_OBJECT – 0x73」特徵,要麼包含如下數據:

從以上輸出中,我們可以看到流數據的最後一部分內容為「nb.deser.HashRequest」對象。讀取這個對象的位置正是交換過程的最後一部分,這也解釋了為什麼漏洞利用函數位於代碼的末尾。現在我們已經知道漏洞利用載荷的存放位置,我們怎麼樣才能生成並發送載荷呢?

DeserLab本身的代碼其實沒有包含任何可利用的東西,具體原因下文會解釋,現在我們只需要接受這個事實即可。這意味著我們需要查找其他程序庫,從中挖掘能幫助我們的代碼。DeserLab僅僅包含一個Groovy庫,這足以給我們足夠多的提示來生成ysoserial載荷。在現實世界中,我們往往需要親自反彙編未知程序庫,才能尋找到有用的代碼,這些代碼也可以稱為漏洞利用的小工具(gadget)。

掌握庫信息後,載荷的生成就會變得非常簡單,命令如下所示:

需要注意的是,載荷發送後不會返回任何響應,因此如果我們想確認載荷是否工作正常,我們需要一些方法來檢測。在實驗環境中,一個ping localhost命令足以,但在實際環境中,我們需要找到更好的方式。

現在萬事俱備,是不是只需要發送載荷就可以大功告成?差不多是這個樣子,但我們不要忘了Java序列化頭部交換過程在這之前已經完成,這意味著我們需要剔除載荷頭部的前4個字節,然後再發送載荷:

如果一切順利的話,你可以看到如下輸出:

非常好,我們成功利用了DeserLab的漏洞。接下來我們需要好好理解一下我們發往DeserLab的載荷的具體內容。

三、手動構建載荷

想要理解載荷的工作原理,最好的方法就是自己手動重建一模一樣的載荷,也就是說,我們需要寫Java代碼。問題是,我們需要從何處開始?正如我們前面對pcap的分析一樣,我們可以觀察一下序列化載荷。使用如下這條命令,我們可以將載荷轉換為十六進位字符串,然後我們就可以使用SerializationDumper來分析這個字符串,當然如果你喜歡的話,你也可以使用jdeserialize來分析文件。

現在我們可以深入分析一下,理解具體的工作過程。話說回來,當理清這些問題後,你可能會找到另一篇文章詳細介紹了整個過程,所以如果願意的話,你可以跳過 這部分內容,直接閱讀這篇文章。接下來的文章著重介紹了我所使用的方法。在我使用的方法中,非常重要的一點就是閱讀ysoserial中關於這個漏洞利用部分的源碼。我不想重複提及這一點,如果你納悶我怎麼找到具體的工作流程,我會讓你去閱讀ysoserial的實現代碼。

將載荷傳給工具處理後,這兩個工具都會生成非常長的輸出信息,包含各種Java類代碼。其中我們主要關注的類是輸出信息中的第一個類,名為「sun.reflect.annotation.AnnotationInvocationHandler」。這個類看起來非常眼熟,因為它是許多反序列利用代碼的入口點。我還注意到其他一些信息,包括「java.lang.reflect.Proxy」、「org.codehaus.groovy.runtime.ConvertedClosure」以及 「org.codehaus.groovy.runtime.MethodClosure」。這些類之所以引起我的注意,原因在於它們引用了我們用來利用漏洞的程序庫,此外,網上關於Java反序列化漏洞利用的文章中也提到過這些類,我在ysoserial源碼中也見過這些類。

我們需要注意一個重要概念,那就是當你在執行反序列化攻擊操作時,你發送的實際上是某個對象的「已保存的」狀態。也就是說,你完全依賴於接收端的行為模式,更具體地說,你依賴於接收端在反序列化你發送的「已保存的」狀態時所執行的具體操作。如果另一端沒有調用你所發送的對象中的任何方法,你就無法達到遠程代碼執行目的。這意味著你唯一能改變的只是操作對象的屬性信息。

理清這些概念後我們可知,如果我們想獲得代碼執行效果,我們所發送的第一個類中的某個方法需要被自動調用,這也解釋了為什麼第一個類的地位如此重要。如果我們觀察AnnotationInvocationHandler的代碼,我們可以看到其構造函數接受一個java.util.map對象,且readObject方法會調用Map對象上的一個方法。如果你閱讀過其他文章,那麼你就會知道,當數據流被反序列化時會自動調用readObject方法。基於這些信息,再從其他文章來源借鑑部分代碼,我們就可以著手構建自己的漏洞利用代碼,如下所示。如果你想理解代碼內容,你可以先參考一下Java中的反射(reflection)機制。

你可以使用如下命令來編譯並運行這段代碼,雖然目前它還沒有什麼實際功能:

當你拓展這段代碼的功能時,請牢記以下幾點:

碰到錯誤代碼時請及時Google;

類名需與文件名保持一致;

請熟練掌握Java語言。

上述代碼可以提供可用的初始入口點類以及構造函數,但我們具體需要往構造函數中傳遞什麼參數呢?大多數例子中會使用如下這行代碼:

對於「map」參數我的理解是,首次調用readObject期間會調用map對象的「entrySet」方法。我不是特別明白第一個參數的內部工作機制,但我知道readObject方法內部會檢查這個參數,以確認該參數為「AnnotionType」類型。我們為該參數提供了一個「Override」類,可以滿足類型要求。

現在說到重點了。為了理解程序的工作原理,我們需要注意的是,第二個參數不是一個簡單的Java map對象,而是一個Java代理(Proxy)對象。我第一次接觸到這個事實時也不明白這有什麼具體含義。有一篇文章)詳細介紹了Java動態代理(Dynamic Proxies)機制的相關內容,也提供了非常好的示例代碼。文章部分內容摘抄如下:

「 通過動態代理機制,僅包含1個方法的單一類可以使用多個調用接口為包含任意多個方法的任意類提供服務。動態代理的作用與封裝(Facade)層類似,但你可以把它當成是任意接口的具體實現。拋去外表後,你會發現動態代理會把所有的方法調用導向單獨的一個處理程序,即invoke()方法。 」

簡單理解的話,代理對象可以假裝成一個Java map對象,然後將所有對原始Map對象的調用導向對另一個類的某個方法的調用。讓我們用一張圖來梳理一下:

這意味著我們可以使用這種Map對象來拓展我們的代碼,如下所示:

需要注意的是,我們仍然需要匹配代碼中的invocationhandler,現在我們還沒填充這個位置。這個位置最終由Groovy來填充,目前為止我們仍停留在普通的Java類範圍內。Groovy之所以適合這個位置,原因在於它包含一個InvocationHandler。因此,當InvocationHandler被調用時,程序最終會引導我們達到代碼執行效果,如下所示:

如你所見,上面代碼中我們在invocationhandler填入了一個ConvertedClosure對象。你可以反編譯Groovy庫來確認這一點,當你觀察ConvertedClosure類時,你可以看到它繼承(extends )自ConversionHandler類,反編譯這個類,你可以看到如下代碼:

從代碼中我們可知,ConversionHandler實現了InvocationHandler,這也是為什麼我們可以在代理對象中使用它的原因所在。當時我不能理解的是Groovy載荷如何通過Map代理來實現代碼執行。你可以使用反編譯器來查看Groovy庫的代碼,但通常情況下,我發現使用Google來搜索關鍵信息更為有效。比如說,這種情況下,我們可以在Google中搜索如下關鍵詞:

搜索上述關鍵詞後,我們可以找到許多文章來解釋這個問題,比如這篇文章以及這篇文章。這些解釋的要點在於,String對象有一個名為「execute」的附加方法。我經常使用這種查詢方法來處理我不熟悉的那些環境,因為對開發者而言,執行shell命令通常是一個剛需,而相關答案又經常可以在網際網路上找到。理解這一點後,我們可以使用一張圖來完整表達載荷的工作原理,如下所示:

你可以訪問此連結獲取完整版代碼,然後使用如下命令編譯並運行這段代碼:

運行這段代碼後,我們應該能夠得到與ysoserial載荷一樣的結果。令我感到驚奇的是,這些載荷的哈希值竟然完全一樣。

感謝大家閱讀本文,希望以後在利用Java反序列化漏洞的過程中,大家也能更好地理解漏洞利用原理。

*本文轉載自安全客

相關焦點

  • Java安全之反序列化漏洞分析
    序列化與反序列化對於Java程式設計師來說,應該不算陌生了,序列化與反序列化簡單來說就是Java對象與數據之間的相互轉化。那麼對於完全面向對象的Java語言來說為什麼要有序列化機制?實質上,序列化機制並不只局限於Java語言,序列化的本質是內存對象到數據流的一種轉換,我們知道內存中的東西不具備持久性,但有些場景卻需要將對象持久化保存或傳輸。
  • 文庫 | 反序列化漏洞匯總
    不安全的反序列化是指網站對用戶可控制的數據進行反序列化時,攻擊者能夠操縱序列化的對象,以將有害數據傳遞到應用程式代碼中。甚至有可能用完全不同類的對象替換序列化的對象。更誇張的是,將對網站可用的任何類別的對象進行反序列化和實例化,而與預期的類別無關。因此,不安全的反序列化有時稱為「對象注入」漏洞。意外類的對象可能會導致異常。但是,到此時,損壞可能已經造成。許多基於反序列化的攻擊是在反序列化完成之前完成的。
  • JAVA反序列化—FastJson抗爭的一生
    No.2前言其實從一開始就是想著學一下fastjson組件的反序列化。結果發現完全理解不能。(此時由於默認白名單的引入,漏洞危害大降)6.到了1.2.47通殺黑白名單漏洞,因為網上對於這個分析文有點過多。這邊想著直接正向來沒得意思。嘗試從代碼審計漏洞挖掘的角度去從零開始挖掘出這一條利用鏈。
  • PHP反序列化漏洞說明
    序列化可以將對象轉換成字符串,但僅保留對象裡的成員變量,不保留函數方法。PHP序列化的函數為serialize,反序列化的函數為unserialize.舉個慄子:<?反序列化反序列化就是序列化的逆過程,即對於將對象進行序列化後的字符串,還原其成員變量的過程。接上述慄子:<?
  • 【Java拾遺】不可不知的 Java 序列化
    所以序列化可以簡單的理解成是 機器內存中信息的表達方式 。答案就是把內存中的對象保存到磁碟上,這樣就不怕重啟了,而持久化就需要用到序列化技術。如何實現序列化人與人之間有許許多多的表達方式,而且機器與機器之間也同樣,序列化的方式多種多樣。
  • 全方位解析Java序列化
    ,於是 Web 容器就會把一些 Session 先序列化到硬碟中,等要用了,再把保存在硬碟中的對象還原到內存中。java.io.ObjectInputStream表示對象輸入流,它的 readObject() 方法,從輸入流中讀取到字節序列,反序列化成為一個對象,最後將其返回。五、序列化的使用序列化如何使用?
  • MySQL JDBC客戶端反序列化漏洞
    標題: MySQL JDBC客戶端反序列化漏洞☆ 背景介紹☆ 學習思路☆ 搭建測試環境☆ 惡意MySQL插件    1) 獲取MySQL 5.7.28源碼    2) 在rewrite_example基礎上修改出evilreplace☆ 測試rewriter插件    1) 安裝rewriter.so
  • 原理+實踐掌握(PHP反序列化和Session反序列化)
    本文轉自先知社區:https://xz.aliyun.com/t/7366前言:最近又接觸了幾道php反序列化的題目,覺得對反序列化的理解又加深了一點,這次就在之前的學習的基礎上進行補充。在解序列化一個對象前,這個對象的類必須在解序列化之前定義。簡單來理解起來就算將序列化過存儲到文件中的數據,恢復到程序代碼的變量表示形式的過程,恢復到變量序列化之前的結果。$s = file_get_contents(『.
  • Java程式設計師必備:序列化全方位解析
    java.io.ObjectInputStream表示對象輸入流,它的readObject()方法,從輸入流中讀取到字節序列,反序列化成為一個對象,最後將其返回。五、序列化的使用序列化如何使用?列印學生對象,序列化到文件,接著修改靜態變量的值,再反序列化,輸出反序列化後的對象~
  • 什麼是序列化,怎麼序列化,為什麼序列化,反序列化會遇到什麼問題,如何解決.
    一、序列化和反序列化的概念 序列化:把對象轉換為字節序列的過程稱為對象的序列化。反序列化:把字節序列恢復為對象的過程稱為對象的反序列化。上面是專業的解釋,現在來點通俗的解釋。FlyPig 對象序列化成功!FlyPig 對象反序列化成功!
  • 支付寶面試:什麼是序列化,怎麼序列化,為什麼序列化,反序列化會遇到什麼問題,如何解決?
    FlyPig 對象序列化成功!FlyPig 對象反序列化成功!FlyPig 對象反序列化成功!序列化的時候,是沒的那個屬性的,在發序列化的時候,對應的model多了個屬性,但是,反序列化執行OK,沒出異常。
  • 看代碼學安全(11) - unserialize反序列化漏洞
    漏洞解析:(上圖代碼第11行正則表達式應改為:』/O:d:/『)題目考察對php反序列化函數的利用。代碼 11行 ,第一個if,截取前兩個字符,判斷反序列化內容是否為對象,如果為對象,返回為空。php可反序列化類型有String,Integer,Boolean,Null,Array,Object。去除掉Object後,考慮採用數組中存儲對象進行繞過。
  • 教你徹底學會Java序列化和反序列化
    Java序列化是指把Java對象轉換為字節序列的過程,Java反序列化是指把字節序列恢復為Java對象的過程。反序列化:客戶端重文件,或者網絡中獲取到文件以後,在內存中重構對象。序列化:對象序列化的最重要的作用是傳遞和保存對象的時候,保證對象的完整性和可傳遞性。方便字節可以在網絡上傳輸以及保存在本地文件。
  • Weblogic IIOP反序列化分析(CVE-2020-2551)
    No.2 漏洞概述2020年1月15日, Oracle官方發布了CVE-2020-2551的漏洞通告,漏洞等級為高危,CVVS評分為9.8分,漏洞利用難度低。影響範圍為10.3.6.0.0, 12.1.3.0.0, 12.2.1.3.0, 12.2.1.4.0。
  • java序列化反序列化中serialVersionUID到底有什麼用
    序列化\反序列化:java序列化是指把java對象轉換為字節序列的過程,而java反序列化是指把字節序列恢復為java對象的過程。用途:當兩個進程進行遠程通信時,可以相互發送各種類型的數據,包括文本,圖片,音頻,視頻等,而這些數據都會以二進位的形式在網絡上傳送;當兩個java進行進行通信時,要傳送對象,怎麼傳對象,通過序列化與反序列化;永久性保存對象,保存對象的字節序列到本地文件或者資料庫中,實現了數據的持久化;利用序列化實現遠程通信,可以在網絡上傳送對象的字節序列
  • Yii2 反序列化漏洞復現分析
    1、漏洞描述Yii是一套基於組件、用於開發大型Web應用的高性能PHP框架。Yii2 2.0.38 之前的版本存在反序列化漏洞,程序在調用unserialize() 時,攻擊者可通過構造特定的惡意請求執行任意命令。
  • PHP反序列化漏洞簡介及相關技巧小結
    要學習PHP反序列漏洞,先了解下PHP序列化和反序列化是什麼東西。
  • 明晰 | Java序列化與反序列化
    實戰含義序列化:對象寫入IO流中,實現對象變成文件。反序列化:把文件中的對象,進行恢復,恢復到內存中,實現反序列化。意義:序列化的最大的意義在於實現對象可以跨主機進行傳輸,這些對象可以實現保存在磁碟中,並且脫離程序而獨立存在。
  • 為什麼Java中序列化的SerialVersionUID總是無意義的?
    一、serialVersionUID的作用通過java進行網絡之間的數據傳輸是不能直接把對象進行傳的,需要在發送端把數據切分,在接收端對切分的數據進行重裝。這種切分和重裝的方式就叫做序列化。:(1)發送端不指定serialVersionUID,編譯器為我們默認生成,並序列化保存在流中發送到接收端。
  • Oracle:Java 的序列化就是個錯誤,我們要刪掉它!
    此前,Java提供了一種機制叫做序列化,即通過有序的格式或字節序列持久化Java對象,包括對象的數據、類型,以及保存在對象中的數據類型。簡單來說,就是把對象轉換為字節序列的過程。萬萬沒想到,若干年後,Oracle意識到了Java存在的序列化漏洞,並決心剔除該特性!