自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

HTTP被動掃描代理的那些事

安全 應(yīng)用安全 網(wǎng)絡(luò)管理
這篇文章我們從小白的角度粗略的聊聊 HTTP 代理到底是如何工作的,在實現(xiàn)被動掃描功能時有哪些細節(jié)需要注意以及如何科學(xué)的處理這些細節(jié)。

HTTP 代理這個名詞對于安全從業(yè)人員應(yīng)該都是熟知的,我們常用的抓包工具 burp 就是通過配置 HTTP 代理來實現(xiàn)請求的截獲修改等。然而國內(nèi)對這一功能的原理類文章很少,有的甚至有錯誤。筆者在做 xray 被動代理時研究了一下這部分內(nèi)容,并整理成了這篇文章,這篇文章我們從小白的角度粗略的聊聊 HTTP 代理到底是如何工作的,在實現(xiàn)被動掃描功能時有哪些細節(jié)需要注意以及如何科學(xué)的處理這些細節(jié)。

[[276450]]

開始之前我先來一波靈魂6問,讀者可以先自行思考下,這些問題將是本文的關(guān)鍵點,并將在文章中一一解答:

  • http_proxy 和 https_proxy 有什么區(qū)別?
  • 為什么需要信任證書才能掃描 HTTPS 的站點?
  • 代理 HTTPS 的站點一定需要信任證書嗎?
  • 代理的隧道模式下如何區(qū)分是不是 TLS 的流量?
  • 代理應(yīng)如何處理 Websocket 和 HTTP2 的流量?
  • 是否應(yīng)該復(fù)用連接以及如何復(fù)用連接?

知識儲備

我們在本地做開發(fā)時,有時會需要啟動一個 HTTPS 的服務(wù),通常使用 OpenSSL 自行簽發(fā)證書并在系統(tǒng)中信任該證書,然后就可以正常使用這個 TLS 服務(wù)了。如果沒有信任,瀏覽器就會提示證書不信任而無法訪問,簡言之,我們需要手動信任自行簽發(fā)的證書才可以正常訪問配置了該證書的網(wǎng)站。那么問題來了,為什么平日訪問的那些網(wǎng)站都不需要信任證書呢?打開 baidu.com 查看其證書發(fā)現(xiàn)這里其實是一個證書鏈:

 

最頂層的 Global Sign RootCA 是一個根證書,第二個是一個中間證書,最后一個才是 baidu 的頒發(fā)證書,這三種證書的效力是:

  1. RootCA >  Intermediates CA > End-User Cert 

而且只要信任了 RootCA 由 RootCA 簽發(fā)的包括其下級簽發(fā)的證書都會被信任。而 Global Sign RootCA等是一些默認安裝在系統(tǒng)和瀏覽器中的根證書。這些證書由一些權(quán)威機構(gòu)來維護,可以確保證書的安全和有效性。而內(nèi)置的這些根證書就允許我們訪問一些公共的網(wǎng)站而無需手動信任證書了。

再來說下與 HTTP 代理相關(guān)的兩個環(huán)境變量: HTTP_PROXY 和 HTTPS_PROXY,有的程序使用的是小寫的,比如 curl。對于這兩個變量,約定俗稱的規(guī)則如下:

  • 如果目標是 HTTP 的,則使用 HTTP_PROXY 中的地址
  • 如果目標是 HTTPS 的,則使用 HTTPS_PROXY 中的地址
  • 如果對應(yīng)的環(huán)境變量為空,則不使用代理

這兩個環(huán)境變量的值是一個 URI,常見的有如下三種形式:

  • http://127.0.0.1:7777
  • https://127.0.0.1:7777
  • socks5://127.0.0.1:7777

拋開與主題無關(guān)的 socks 不管,這里又有一個 http 和 https,別暈,這里的 http 和 https 指的是代理服務(wù)器的類型,類似 http://baidu.comhttps://baidu.com 一個是裸的 HTTP 服務(wù),一個套了一層 TLS 而已。那么組合一下就有 4 種情況了:

  • http_proxy=http://127.0.0.1:7777
  • https_proxy=http://127.0.0.1:7777
  • http_proxy=https://127.0.0.1:7777
  • https_proxy=https://127.0.0.1:7777

這四種情況都是合法的,也是代理實現(xiàn)時應(yīng)該考慮的。但是如上面所說,這只是約定俗稱的,沒有哪個 RFC 規(guī)定必須這樣做,導(dǎo)致上面四種情況在常見的工具中被實現(xiàn)的五花八門,為了避免把大家繞暈,我直接說結(jié)論:很多工具對后面兩種不支持,比如 wget, python requests, 也就是說 https://還是被當成了 http://,因此我們這里只討論前兩種情況的實現(xiàn)。

代理中的 MITM

HTTP 代理的協(xié)議基于 HTTP,因此 HTTP 代理本身就是一個 HTTP 的服務(wù),而其工作原理本質(zhì)上就是中間人(MITM) ,即讀取當前客戶端的 HTTP 請求,從代理發(fā)送出去并獲得響應(yīng),然后將響應(yīng)返回給客戶端。其過程類似下面的流程:

為了更直觀的感受下,可以用 nc 監(jiān)聽 127.0.0.1:7777 然后使用:

  1. httphttp_proxy=http://127.0.0.1:7777 curl http://example.com 

會發(fā)現(xiàn) nc 的數(shù)據(jù)包為:

  1. GET http://example.com/ HTTP/1.1 
  2. Host: example.com 
  3. Proxy-Connection: keep-alive 
  4. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4)  
  5. Accept: text/html 
  6. Accept-Encoding: gzip, deflate 
  7. Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 

看起來和 HTTP 的請求非常像,唯一的區(qū)別就是 GET 后的是一個完整的 URI,而不是 path,這主要是方便代理得到客戶端的原始請求,如果不用完整的 URI,請求的 Scheme 將無從得知,端口號有時也可能是不知道的。

在 Go 中我們可以用幾行簡單的代碼實現(xiàn)這種場景下的代理。

  1. package main 
  2. import ( 
  3.    "bufio"    
  4.     "log"    
  5.     "net"    
  6.     "net/http" 
  7. var client = http.Client{} 
  8. func main() { 
  9.    listener, err :net.Listen("tcp", "127.0.0.1:7777") 
  10.    if err != nil { 
  11.       log.Fatal(err) 
  12.    } 
  13.    for { 
  14.       conn, err :listener.Accept() 
  15.       if err != nil { 
  16.          log.Fatal(err) 
  17.       } 
  18.       go handleConn(conn) 
  19.    } 
  20. func handleConn(conn net.Conn) { 
  21.    // 讀取代理中的請求   req, err :http.ReadRequest(bufio.NewReader(conn)) 
  22.    if err != nil { 
  23.       log.Println(err) 
  24.       return   } 
  25.    req.RequestURI = ""   // 發(fā)送請求獲取響應(yīng)   resp, err :client.Do(req) 
  26.    if err != nil { 
  27.       log.Println(err) 
  28.       return   } 
  29.    // 將響應(yīng)返還給客戶端   _ = resp.Write(conn) 
  30.    _ = conn.Close() 

編譯運行這段代碼,然后使用 curl 測試下:

  1. httphttp_proxy=http://127.0.0.1:7777  curl -v http://example.com 

代理看起來工作正常,我們使用不到 40 行代碼就實現(xiàn)了一個簡易的 HTTP 代理!代碼中的 req 就是做被動代理掃描需要用到的請求,把請求復(fù)制一份扔給掃描器就可以了。這也就是上面說的第一種情況, 即http_proxy=http://。那么如果直接使用上述實現(xiàn)訪問 https 的站點會發(fā)生什么呢?

TLS 與隧道代理

  1. httphttps_proxy=http://127.0.0.1:7777 curl -v https://baidu.com 

使用上面的方式訪問 baidu 時,出現(xiàn)了比較奇怪的事情——通過代理讀到的客戶端請求不是原來的請求,而是一個 CONNECT 請求:

  1. CONNECT baidu.com:443 HTTP/1.1 
  2. Host: baidu.com:443 
  3. User-Agent: curl/7.54.0 
  4. Proxy-Connection: Keep-Alive 

這是 HTTP 代理的另一種形式,稱為隧道代理。隧道代理的過程如下:

隧道代理的出現(xiàn)是為了能在 HTTP 協(xié)議基礎(chǔ)上傳輸非 HTTP 的內(nèi)容。如果你用過 websocket,一定對 Connection: Upgrade 這個頭不陌生。這個頭是用來告訴 server,客戶端想把當前的 HTTP 的連接轉(zhuǎn)為 Websocket 協(xié)議通信的連接。類似的,這里的 CONNECT是一種協(xié)議轉(zhuǎn)換的請求,但這種轉(zhuǎn)換更像是一種 Degrade,因為握手完成后,這個鏈接將退化為原始的 Socket Connection,可以在其中傳輸任意數(shù)據(jù)。用文字描述下整個過程如下:

  • 客戶端想通過代理訪問https://baidu.com,向代理發(fā)送 Connect 請求。
  • 代理嘗試連接 baidu.com:443,如果連接成功返回一個 200 響應(yīng),連接控制權(quán)轉(zhuǎn)交個客戶端;如果連接失敗返回一個 502,連接中止。
  • 客戶端收到 200 后,在這個連接中進行 TLS 握手,握手成功后進行正常的 HTTP 傳輸。

有個點需要注意下,轉(zhuǎn)換后的連接是可以傳輸任意數(shù)據(jù)的,并非只是 HTTPS 流量,可以是普通的 HTTP流量,也可以是其他的應(yīng)用層的協(xié)議流量。那么我們回到被動代理掃描這個話題,如何獲取隧道代理中的請求并用來掃描?

這是一個比較棘手的問題,正是由于隧道中的流量可以是任意應(yīng)用層協(xié)議的數(shù)據(jù),我們無法確切知道隧道中流量用的哪種協(xié)議,所以只能猜一下。查看 TLS 的 RFC 可以發(fā)現(xiàn),TLS 協(xié)議開始于一個字節(jié) 0×16,這個字節(jié)在協(xié)議中被稱為 ClientHello,那么我們其實就可以根據(jù)這第一個字節(jié)將協(xié)議簡單區(qū)分為 TLS 流量和非 TLS 流量。對于被動掃描器而言,為了簡單起見,我們認為 TLS 的流量就是 HTTPS 流量,非 TLS 流量就是 HTTP 流量。后者和普通代理下的 MITM 一致,可以直接復(fù)用代碼,而 HTTPS 的情況需要多一個 TLS 握手的過程。用偽代碼表示就是:

  1. b = conn.Read(1) 
  2. if b == "0x16" { 
  3.     tlsHandShake(conn) 
  4. }  
  5. req = readRequest(conn) 
  6. handleReq(conn, req) 

這里有個細節(jié)是讀出的這一個字節(jié)不要忘記“塞回去”,因為少了一個字節(jié),后面的會操作會失敗。

這里我們需要重點關(guān)注下 TLS 握手過程。在 TLS 握手過程中會進行證書校驗,如果客戶端訪問的是 baidu.com,server 需要有 baidu.com 這個域的公鑰和私鑰才能完成握手,可是我們手里哪能有 baidu.com的證書(私鑰),那個在文件在 baidu 的服務(wù)器上呢!

解決辦法就是文章最開始說到的信任根證書。信任根證書后,我們可以在 TLS 握手之前直接簽發(fā)一個對應(yīng)域的證書來進行 TLS 握手,這就是包括 burp 在內(nèi)的所有需要截獲 HTTPS 數(shù)據(jù)包的軟件都需要信任一個根證書的原因!有了被系統(tǒng)信任的根證書,我們就可以簽出任意的被客戶系統(tǒng)信任的具體域的證書,然后就可以剝開 TLS 拿到被動掃描需要的請求了。這里還有一個小問題是簽發(fā)的證書的域該使用哪個,簡單起見我們可以直接使用 CONNECT 過程中的地址,更科學(xué)的方法我們后面說。簽完證書就可以完成 TLS 握手,然后就又和第一節(jié)的情況類似了。

有個點需要提一下,如果不需要進行中間人獲取客戶端請求,是不需要信任證書的,因為這種情況下的是真正的隧道,像是客戶端與服務(wù)器的直接通信,代理服務(wù)器僅僅在做二進制的數(shù)據(jù)轉(zhuǎn)發(fā)。

至此,被動代理的核心實現(xiàn)已經(jīng)完成了,接下來是一些瑣碎的細節(jié),這些細節(jié)同樣值得注意。

代理的認證

一個公網(wǎng)的代理如果沒有加認證是比較危險的,因為代理本身就相當于開放了某個網(wǎng)絡(luò)的使用權(quán)限,而且由于隧道模式的存在,代理的支持的協(xié)議理論上拓寬到了任何基于 TCP 的協(xié)議,如果可以和傳統(tǒng)的 redis 未授權(quán),SSRF DNS rebinding 等結(jié)合一下就是一個簡單的 CTF 題。所以給代理加上鑒權(quán)是很有必要的。

代理的認證和正常的 HTTP Basic Auth 很像,只是相關(guān)頭加了一個 Proxy- 的前綴,可以參考 《HTTP 權(quán)威指南》中的一個圖學(xué)習一下:

點對點的修正

根據(jù) RFC,HTTP 中的下列頭被稱為單跳頭(Hop-By-Hop header),這些 Header 應(yīng)該只作用于單個 TCP 連接的兩端,HTTP 代理在請求中如果遇到了,應(yīng)當刪掉這些頭。

  1. "Proxy-Authenticate", 
  2. "Proxy-Authorization", 
  3. "Connection", 
  4. "Keep-Alive", 
  5. "Proxy-Connection",  
  6. "Te", 
  7. "Trailer", 
  8. "Transfer-Encoding", 
  9. "Upgrade", 

至于這些頭要刪掉的原因,這里按我的理解簡單說下。前兩個是和認證相關(guān)的,每個代理的認證是獨立的,所以認證成功應(yīng)該刪掉當前代理的認證信息。

中間的三個是用于控制連接狀態(tài)的,TCP 連接是端到端的,連接狀態(tài)的維護也應(yīng)該是針對兩端的,即客戶端與代理服務(wù)器, 代理服務(wù)器與目的服務(wù)器應(yīng)該是分別維護各自狀態(tài)的。Proxy-Connection 類似 Connection,是用來指定客戶端和代理之間的連接是不是 KeepAlive 的,代理實現(xiàn)時應(yīng)該兼顧這個要求。對于連接的狀態(tài)管理,我認為比較科學(xué)的方式是分拆而后串聯(lián)。分拆是說 client->proxy 和 proxy -> server 這兩個過程分開處理, client->proxy 的過程每次開啟新的 TCP 連接,不做連接復(fù)用;而 proxy->server 的過程本質(zhì)上就是一個普通的 http 請求,所以可以套一個連接池,借助連接池可以復(fù)用 TCP 連接。兩部分的連接都撥通后,可以將其串聯(lián)起來,最終效果上就是在遵循 Proxy-Connection 的前提下連接的狀態(tài)最終與代理無關(guān),而是由 client 和 server 共同控制。串聯(lián)過程在 Go 中可以用兩行代碼簡單搞定:

  1. go io.Copy(conn1, conn2) 
  2. io.Copy(conn2, conn1) 

TE Trailer Transfer-Encoding和請求傳輸?shù)姆绞接嘘P(guān)。代理在讀取客戶端請求時應(yīng)該確保正確處理了 chunked 的傳輸方式后再刪除這幾個頭,由代理自行決定在發(fā)往目的服務(wù)器時要不要使用分塊傳輸。類似的還有 Content-Encoding,這個決定的是請求的壓縮方式,也應(yīng)該在代理端被科學(xué)的處理掉。好在傳輸方式這幾個頭在 Go 的標準庫中都有實現(xiàn),對開發(fā)者基本都是透明的,開發(fā)者可以直接使用而無需關(guān)心具體的邏輯。

Websocket 與 HTTP2

前面提到過 Upgrade,這里再簡單說說。這個頭常用于從 HTTP 轉(zhuǎn)換到 Websocket 或 HTTP2 協(xié)議。對于 Websocket,被動掃描時可以不關(guān)注,所以可以直接放行。這里放行的意思是不再去解析,而是類似 Tunnel 那種,單純的進行數(shù)據(jù)轉(zhuǎn)發(fā)。對于 HTTP2 ,我們可以拒絕這一轉(zhuǎn)換,使得數(shù)據(jù)協(xié)議始終用 HTTP,也算是一個偷懶的捷徑。

當然,如果想要做的完善些,就需要套用一下這兩種協(xié)議的解析,偽裝成 Websocket server 或 HTTP2 server,然后做中間人去獲取傳輸數(shù)據(jù),有興趣可以看一下 Python 的 MitmProxy 的實現(xiàn)。

離完美的差距

回顧剛才說的一些要點,這里的被動代理實現(xiàn)其實并不完美,主要有這兩點:

第一點是隧道模式下,我們強行判定了以 0×16 開頭的就是 TLS 流量,協(xié)議千千萬,這種可能有誤判的。其次我們認為 TLS 層下的應(yīng)用協(xié)議一定是 HTTP,這也是不妥的,但對于被動掃描這種場景是足夠了。

另一點是隧道模式下證書的簽發(fā)流程不夠完美。如果你用過虛擬主機,或者嘗試過在同一地址同一端口上運行多個 HTTP 服務(wù),那一定知道 nginx 中的 server_name 或是 apache 的 VirtualHost。服務(wù)器收到 HTTP 請求后會去查看請求的 Host 字段,以此決定使用哪個服務(wù)。TLS 模式下有所不同,因為 TLS 握手時服務(wù)器沒法讀取請求,為此 TLS 有個叫 SNI(Server Name Indication)的拓展解決了這個問題,即在 TLS 握手時發(fā)送客戶端請求的域給服務(wù)器,使得在同一 ip 同一端口上運行多個 TLS 服務(wù)成為了可能?;氐奖粍哟磉@,之前我們簽證書用的域是從 CONNECT 的 HOST 中獲取的,其實更好的辦法是從 TLS 的握手中讀取,這樣就需要自行實現(xiàn) TLS 的握手過程了,具體可以參考下 MitmProxy 的實現(xiàn)。

https://docs.mitmproxy.org/stable/concepts-howmitmproxyworks/

責任編輯:趙寧寧 來源: FreeBuf
相關(guān)推薦

2021-10-26 11:42:51

系統(tǒng)

2014-06-06 16:08:17

初志科技

2011-09-19 15:40:35

2020-07-29 08:14:59

云計算云遷移IT

2012-05-31 09:53:38

IT風云15年

2011-05-19 16:47:50

軟件測試

2012-05-01 08:06:49

手機

2024-02-04 17:03:30

2017-05-15 21:50:54

Linux引號

2015-08-20 09:17:36

Java線程池

2021-03-18 16:05:20

SSD存儲故障

2021-08-11 21:46:47

MySQL索引join

2009-02-19 10:21:00

路由多WAN口

2015-09-14 09:28:47

2017-03-08 08:53:44

Git命令 GitHub

2012-10-08 11:55:05

2015-08-13 10:54:46

2012-07-13 00:03:08

WEB前端開發(fā)WEB開發(fā)

2010-07-27 11:29:43

Flex

2017-11-28 15:24:14

ETA配送構(gòu)造
點贊
收藏

51CTO技術(shù)棧公眾號