不按順序來的 TCP 包
關於 TCP 建立連線和斷開連線的流程,很多人都能大致說出來,可以參考 ofollow,noindex" target="_blank">協議森林
正常的資料傳輸是在三次握手結束之後進行的,但是如果打破了這個流程,資料傳輸仍然可能成功,而部分防火牆 IDS 就可能被繞過,下面的兩個例子來自 https://github.com/kirillwow/ids_bypass。
CVE-2018-6794
# 客戶端開始三次握手 傳送 SYN Client->[SYN] [Seq=0 Ack=0]->Evil Server # 伺服器端正常的響應了 SYN-ACK Client<-[SYN, ACK] [Seq=0 Ack=1]<-Evil Server # 但是伺服器端在握手結束之前就傳送了 PSH,裡面包含了一些資料 Client<-[PSH, ACK] [Seq=1 Ack=1]<-Evil Server # 伺服器端主動關閉了連線 Client<-[FIN, ACK] [Seq=83 Ack=1]<-Evil Server # 三次握手完成 Client->[ACK] [Seq=1 Ack=84]->Evil Server # 客戶端正常的傳送資料 Client->[PSH, ACK] [Seq=1 Ack= 4]->Evil Server
Suricata IDS 在 4.0.4 版本之前存在這個問題
RST 導致的繞過
有些 Windows 客戶端在收到 RST 包之後,如果緊接著又收到了其他的 TCP 資料,那仍然是可以讀取和處理的,有些 IDS 正確處理了這個問題,有的在收到 RST 包之後就停止了檢查 TCP 包。
# Client starts a TCP 3-way handshake Client->[SYN] [Seq=0 Ack=0]->Evil Server # Server responses with TCP RST Client<-[RST, ACK] [Seq=0x0 Ack=1]<-Evil Server # And SYN-ACK shortly after RST Client<-[SYN, ACK] [Seq=1 Ack=1]<-Evil Server ... 三次握手繼續 ...
Suricata IDS(全版本?)存在這個問題。對於 UDP 資料包,也有一個類似的問題。
應用
某些雲伺服器廠商會實時的去過濾每臺機器的 HTTP 請求的域名,也就是 Host 欄位,一旦發現是沒有[[(備)]]案的,就會返回一個攔截頁面,怎麼繞過這個呢。經過測試發現某雲應該是不檢測 HTTPS的,如果可以讓 80 埠重定向到 443,然後設定 HSTS 頭,這樣基本長時間內瀏覽器就不會再訪問 80 埠了,雖然 SSL SNI 和 證書中也是含有域名資訊的。
訪問 80 埠,發現三次握手是正常進行的,而攔截髮生在客戶端傳送了 HTTP 請求包之後,這也說明,防火牆不是無條件封禁的和遮蔽埠的,而是實時的過濾。如果可以搶在防火牆發包之前傳送,那就可以實現重定向了。
寫了一個 Python 的指令碼來完成這個事情
# coding=utf-8 from scapy.all import IP, TCP, send, sniff SERVER_DOMAIN = "example.me" SERVER_PORT = 4445 FIN = 0x01 SYN = 0x02 ACK = 0x10 def build_synack(syn): seq = 1 # 確認 SYN ack = syn[TCP].seq + 1 ip = IP(src=syn[IP].dst, dst=syn[IP].src) tcp = TCP( sport=syn[IP].dport, dport=syn[TCP].sport, flags="SA", seq=seq, ack=ack, options=[("MSS", 1460)] ) return ip / tcp def build_finack(syn): """ 帶重定向指令的包 """ seq = 2 ack = syn[TCP].seq + 1 ip = IP(src=syn[IP].dst, dst=syn[IP].src) tcp = TCP( sport=syn[IP].dport, dport=syn[TCP].sport, flags="FA", seq=seq, ack=ack, options=[("MSS", 1460)] ) resp = b"HTTP/1.1 307 Internal Redirect\r\n" \ b"Content-Length: 0\r\n" \ b"Location: https://%s:443\r\n" \ b"Strict-Transport-Security: max-age=31536000\r\n" \ b"\r\n" % SERVER_DOMAIN return ip / tcp / resp def build_ack(p): seq = 3 ack = p[TCP].seq + 1 ip = IP(src=p[IP].dst, dst=p[IP].src) tcp = TCP( sport=p[IP].dport, dport=p[TCP].sport, flags="A", seq=seq, ack=ack, options=[("MSS", 1460)] ) return ip / tcp def handle_packet(p): # 如果是 SYN 就回復 SYN-ACK 和 FIN-ACK if p[TCP].flags & SYN and not p[TCP].flags & ACK: send(build_synack(p)) print("SYN ACK sent") send(build_finack(p)) send("FIN ACK sent") elif p[TCP].flags & FIN and p[TCP].flags & ACK: # 如果不 ACK,客戶端可能一直重傳 send(build_ack(p)) send("ACK sent") if __name__ == "__main__": # 對於 TCP 和 SERVER PORT 埠的包,回撥 handle_packet 函式 sniff(filter="tcp and port %d" % SERVER_PORT, prn=handle_packet)
使用 scapy 框架,監聽一個埠,在接收到 SYN 包之後,按照正常的握手流程返回 SYN-ACK,然後不等接收到 ACK 就繼續傳送 FIN-ACK,告訴客戶端我要斷開連線了,然後在這個包中包含有重定向的 HTTP 包。
在伺服器端視角看是這樣的
在客戶端視角看是這樣的
42 號包是程式碼的重定向,47 號包就是防火牆的重定向,可以看到 TTL 明顯不一致,而且 seq 被我們程式碼擾亂,導致被認為 out-of-order 了。
因為 scapy 是使用者態的,防止核心不知道整個連線流程而傳送 rst 包,可以使用下面的命令遮蔽掉
iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 172.21.0.3 -j DROP
也有人使用核心模組實現了這個功能
https://github.com/ptpt52/hstshack