天天看點

Mitmproxy教程

免費開源的互動式HTTPS代理工具Mitmproxy

本文是一個較為完整的 mitmproxy教程,側重于介紹如何開發攔截腳本,幫助讀者能夠快速得到一個自定義的代理工具。

本文假設讀者有基本的 python 知識,且已經安裝好了一個 python 3 開發環境。如果你對 nodejs 的熟悉程度大于對 python,可移步到 anyproxy,anyproxy 的功能與 mitmproxy 基本一緻,但使用 js 編寫定制腳本。除此之外我就不知道有什麼其他類似的工具了,如果你知道,歡迎評論告訴我。

本文基于 mitmproxy v5,目前版本号為 v5.0.1。

Introduction

顧名思義,mitmproxy 就是用于 MITM 的 proxy,MITM 即中間人攻擊(Man-in-the-middle attack)。用于中間人攻擊的代理首先會向正常的代理一樣轉發請求,保障服務端與用戶端的通信,其次,會适時的查、記錄其截獲的資料,或篡改資料,引發服務端或用戶端特定的行為。

不同于 fiddler 或 wireshark 等抓包工具,mitmproxy 不僅可以截獲請求幫助開發者檢視、分析,更可以通過自定義腳本進行二次開發。舉例來說,利用 fiddler 可以過濾出浏覽器對某個特定 url 的請求,并檢視、分析其資料,但實作不了高度定制化的需求,類似于:“截獲對浏覽器對該 url 的請求,将傳回内容置空,并将真實的傳回内容存到某個資料庫,出現異常時發出郵件通知”。而對于 mitmproxy,這樣的需求可以通過載入自定義 python 腳本輕松實作。

但 mitmproxy 并不會真的對無辜的人發起中間人攻擊,由于 mitmproxy 工作在 HTTP 層,而目前 HTTPS 的普及讓用戶端擁有了檢測并規避中間人攻擊的能力,是以要讓 mitmproxy 能夠正常工作,必須要讓用戶端(APP 或浏覽器)主動信任 mitmproxy 的 SSL 證書,或忽略證書異常,這也就意味着 APP 或浏覽器是屬于開發者本人的——顯而易見,這不是在做黑産,而是在做開發或測試。

事實上,以上說的僅是 mitmproxy 以正向代理模式工作的情況,通過調整配置,mitmproxy 還可以作為透明代理、反向代理、上遊代理、SOCKS 代理等,但這些工作模式針對 mitmproxy 來說似乎不大常用,故本文僅讨論正向代理模式。

Features

  • 攔截HTTP和HTTPS請求和響應并即時修改它們
  • 儲存完整的HTTP對話以供以之後重發和分析
  • 重發HTTP對話的用戶端
  • 重發先前記錄的服務的HTTP響應
  • 反向代理模式将流量轉發到指定的伺服器
  • 在macOS和Linux上實作透明代理模式
  • 使用Python對HTTP流量進行腳本化修改
  • 實時生成用于攔截的SSL / TLS證書
  • And much, much more…

Installation

“安裝 mitmproxy”這句話是有歧義的,既可以指“安裝 mitmproxy 工具”,也可以指“安裝 python 的 mitmproxy 包”,注意後者是包含前者的。

如果隻是拿 mitmproxy 做一個替代 fiddler 的工具,沒有什麼定制化的需求,那完全隻需要“安裝 mitmproxy 工具”即可,去 mitmproxy 官網 上下載下傳一個 installer 便可開箱即用,不需要提前準備好 python 開發環境。但顯然,這不是這裡要讨論的,我們需要的是“安裝 python 的 mitmproxy 包”。

安裝 python 的 mitmproxy 包除了會得到 mitmproxy 工具外,還會得到開發定制腳本所需要的包依賴,其安裝過程并不複雜。

首先需要安裝好 python,版本需要不低于 3.6,且安裝了附帶的包管理工具 pip。這裡不做展開,假設你已經準備好這樣的環境了。

安裝開始。

在 linux 中:

sudo pip3 install mitmproxy
           

在 windows 中,以管理者身份運作 cmd 或 power shell:

pip3 install mitmproxy
           

在macos中:

brew install mitmproxy
           

安裝完成後,系統将擁有

mitmproxy

mitmdump

mitmweb

三個指令,由于

mitmproxy

指令不支援在 windows 系統中運作(這沒關系,不用擔心),我們可以拿

mitmdump

測試一下安裝是否成功,執行:

mitmdump --version
           

應當可以看到類似于這樣的輸出:

Mitmproxy: 5.0.1
Python:    3.8.2
OpenSSL:   OpenSSL 1.1.1f  31 Mar 2020
Platform:  macOS-10.15.3-x86_64-i386-64bit
           

Run

要啟動 mitmproxy 用

mitmproxy

mitmdump

mitmweb

這三個指令中的任意一個即可,這三個指令功能一緻,且都可以加載自定義腳本,唯一的差別是互動界面的不同。

mitmproxy

指令啟動後,會提供一個指令行界面,使用者可以實時看到發生的請求,并通過指令過濾請求,檢視請求資料。形如:

Mitmproxy教程

配置代理的方法與配置burpsuite一樣,我用的谷歌浏覽器(127.0.0.1 8080),然後指令行執行

mitmproxy --listen-host 127.0.0.1 -p 8080
           

之後,浏覽器位址通路

http://mitm.it/

,點選Other下載下傳安裝證書,下圖所示:

Mitmproxy教程
Mitmproxy教程
usage: mitmproxy [options]
#可選參數:

 -h, --help      show this help message and exit
 --version       show version number and exit
 --options       Show all options and their default values
 --commands      顯示所有指令及其簽名
 --set option[=value]	設定一個選項。 省略該值時,布爾值設定為true,字元串和整數設定為None(如果允許),并且序列為空。 布爾值可以為true,false或toggle
 -q, --quiet      Quiet.
 -v, --verbose     增加日志詳細程度
 --mode MODE, -m MODE 	模式可以是“正常”,“透明”,“ socks5”,“反向:SPEC”或“上遊:SPEC”。 對于反向和上遊代理模式,SPEC是主機規範,形式為“ http [s]:// host [:port]”
 --no-anticache
 --anticache     去除可能導緻伺服器傳回304-not-modified的請求頭
 --no-showhost
 --showhost      使用Host标頭構造用于顯示的URL
 --rfile PATH, -r PATH		從檔案讀取流量
 --scripts SCRIPT, -s SCRIPT	執行腳本。 可能會多次通過
 --stickycookie FILTER		設定粘性Cookie過濾條件,根據要求比對
 --stickyauth FILTER  設定粘性身份驗證過濾條件,根據要求比對
 --save-stream-file PATH, -w PATH	流量到達時儲存到檔案(附加路徑)。      
 --no-anticomp
 --anticomp      嘗試令伺服器向我們發送未壓縮的資料。
 --console-layout {horizontal,single,vertical}		控制台布局
 --no-console-layout-headers
 --console-layout-headers		顯示布局元件标題

#代理選項:

 --listen-host HOST  	綁定代理的位址到HOST
 --listen-port PORT, -p PORT	代理服務端口
 --no-server, -n
 --server      啟動代理伺服器( 預設啟用)
 --ignore-hosts HOST		忽略主機并轉發所有流量,而不對其進行處理。 在透明模式下,建議使用IP位址(範圍),而不要使用主機名。 在正常模式下,僅SSL流量會被忽略,應使用主機名。 利用正規表達式解釋提供的值,并與ip或主機名比對
 --allow-hosts HOST 與--ignore-hosts相反
 --tcp-hosts HOST   與--ignore-hosts相反。 對于與該模式比對的所有主機,可以通過通用TCP SSL代理模式。 與--ignore相似,但是SSL連接配接被攔截。 通信内容以詳細模式列印到日志中
 --upstream-auth USER:PASS	通過将HTTP基本身份驗證添加到上遊代理和反向代理請求。 格式:使用者名:密碼
 --proxyauth SPEC	需要代理身份驗證。 格式:“使用者名:密碼”,“任何”以接受任何使用者/密碼組合,“ @ path”以使用Apache htpasswd檔案或用于LDAP認證的“ ldap [s]:url_server_ldap:dn_auth:password:dn_subtree”
 --no-rawtcp
 --rawtcp       啟用/禁用實驗性原始TCP支援。 以非ascii位元組開頭的TCP連接配接将被視為與tcp_hosts比對。 啟發式方法很粗糙,請謹慎使用。 預設禁用
 --no-http2
 --http2        啟用/禁用HTTP / 2支援。 預設情況下啟用HTTP / 2支援

#SSL:

 --certs SPEC     形式為“ [domain =] path”的SSL證書。 該域可以包含通配符,如果未指定,則等于“ *”。 路徑中的檔案是PEM格式的證書。 如果PEM中包含私鑰,則使用私鑰,否則使用conf目錄中的預設密鑰。 PEM檔案應包含完整的證書鍊,并将葉子證書作為第一項
 --no-ssl-insecure
 --ssl-insecure, -k  不要驗證上遊伺服器SSL / TLS證書
 --key-size KEY_SIZE  證書和CA的TLS密鑰大小

#用戶端重發:

 --client-replay PATH, -C PATH		重發來自已儲存檔案的用戶端請求

#服務端重發:

 --server-replay PATH, -S PATH		從儲存的檔案重發伺服器響應
 --no-server-replay-kill-extra
 --server-replay-kill-extra		在重發期間殺死額外的請求。  
 --no-server-replay-nopop
 --server-replay-nopop		使用後,請勿從伺服器重發狀态中删除流量。 這樣可以多次重發相同的響應。 
 --no-server-replay-refresh
 --server-replay-refresh		通過調整日期,到期和最後修改的header頭,以及調整cookie過期來重新整理伺服器重發響應。   

#更換:

 --replacements PATTERN, -R PATTERN		替換形式:替換形式為``/ pattern / regex / replacement'',其中分隔符可以是任何字元。 可能會多次通過。

#設定Headers:

 --setheaders PATTERN, -H PATTERN		格式為“ /pattern/header/value”的标題設定模式,其中分隔符可以是任何字元。

#Filters:

有關過濾條件表達式文法,請參見mitmproxy中的幫助。

 --intercept FILTER  設定攔截過濾表達式。
 --view-filter FILTER 将視圖限制為比對流。
           

mitmdump

是mitmproxy的指令行模式。 它提供了類似tcpdump的功能,可幫助你檢視,記錄和以程式設計方式轉換HTTP流量。 有關完整的文檔,請參見

--help

例1.

mitmdump -w outfile		#儲存流量
           

以代理模式啟動mitmdump,并将所有流量寫入outfile中。

例2.

mitmdump -nr infile -w outfile "~m post"	#儲存過濾後的流量
           

在不綁定代理端口(-n)的情況下啟動mitmdump,從infile中讀取所有流,應用指定的過濾表達式(僅比對POST),然後寫入outfile。

例3.

mitmdump -nc outfile		#用戶端重發
           

在不綁定代理端口(-n)的情況下啟動mitmdump,然後重發outfile(-c filename)中的所有請求。 以較明顯的方式組合的标志,是以您可以重播來自一個檔案的請求,并将結果流寫入另一個檔案:

mitmdump -nc srcfile -w dstfile
           

有關更多資訊,請參見client-side replay部分。

例4.

mitmdump -s examples/add_header.py		#運作一個腳本
           

這将運作add_header.py示例腳本,該腳本僅向所有響應添加新的header頭。

例5.

mitmdump -ns example/add_header.py -r srcfile -w dstfile		#腳本化資料轉換
           

此指令從srcfile加載資料請求,根據指定的腳本對其進行轉換,然後将其寫回到dstfile檔案中。

mitmweb

指令啟動後,會提供一個 web 界面,使用者可以實時看到發生的請求,并通過 GUI 互動來過濾請求,檢視請求資料。形如:

Mitmproxy教程

Scripts

完成了上述工作,我們已經具備了操作 mitmproxy 的基本能力 了。接下來開始開發自定義腳本,這才是 mitmproxy 真正強大的地方。

腳本的編寫需要遵循 mitmproxy 規定的套路,這樣的套路有兩個,使用時選其中一個套路即可。

第一個套路是,編寫一個 py 檔案供 mitmproxy 加載,檔案中定義了若幹函數,這些函數實作了某些 mitmproxy 提供的事件,mitmproxy 會在某個事件發生時調用對應的函數,形如:

import mitmproxy.http
from mitmproxy import ctx

num = 0


def request(flow: mitmproxy.http.HTTPFlow):
    global num
    num = num + 1
    ctx.log.info("We've seen %d flows" % num)
           

第二個套路是,編寫一個 py 檔案供 mitmproxy 加載,檔案定義了變量 addons,addons 是個數組,每個元素是一個類執行個體,這些類有若幹方法,這些方法實作了某些 mitmproxy 提供的事件,mitmproxy 會在某個事件發生時調用對應的方法。這些類,稱為一個個

addon

,比如一個叫 Counter 的 addon:

import mitmproxy.http
from mitmproxy import ctx


class Counter:
    def __init__(self):
        self.num = 0

    def request(self, flow: mitmproxy.http.HTTPFlow):
        self.num = self.num + 1
        ctx.log.info("We've seen %d flows" % self.num)


addons = [
    Counter()
]
           

這裡強烈建議使用第二種套路,直覺上就會感覺第二種套路更為先進,使用會更友善也更容易管理和拓展。況且這也是官方内置的一些 addon 的實作方式。

我們将上面第二種套路的示例代碼存為 addons.py,再重新啟動 mitmproxy:

mitmweb -s addons.py
           

當浏覽器使用代理進行通路時,就應該能看到控制台裡有類似這樣的日志:

Web server listening at http://127.0.0.1:8081/
Loading script addons.py
Proxy server listening at http://*:8080
We've seen 1 flows
……
……
We've seen 2 flows
……
We've seen 3 flows
……
We've seen 4 flows
……
……
We've seen 5 flows
……
           

這就說明自定義腳本生效了。

Events

上述的腳本估計不用我解釋相信大家也看明白了,就是當 request 發生時,計數器加一,并列印日志。這裡對應的是 request 事件,那總共有哪些事件呢?不多,也不少,這裡詳細介紹一下。

事件針對不同生命周期分為 5 類。“生命周期”這裡指在哪一個層面看待事件,舉例來說,同樣是一次 web 請求,我可以了解為“HTTP 請求 -> HTTP 響應”的過程,也可以了解為“TCP 連接配接 -> TCP 通信 -> TCP 斷開”的過程。那麼,如果我想拒絕來個某個 IP 的用戶端請求,應當注冊函數到針對 TCP 生命周期 的

tcp_start

事件,又或者,我想阻斷對某個特定域名的請求時,則應當注冊函數到針對 HTTP 聲明周期的

http_connect

事件。其他情況同理。

1. 針對 HTTP 生命周期

def http_connect(self, flow: mitmproxy.http.HTTPFlow):
           

(Called when) 收到了來自用戶端的 HTTP CONNECT 請求。在 flow 上設定非 2xx 響應将傳回該響應并斷開連接配接。CONNECT 不是常用的 HTTP 請求方法,目的是與伺服器建立代理連接配接,僅是 client 與 proxy 的之間的交流,是以 CONNECT 請求不會觸發 request、response 等其他正常的 HTTP 事件。

def requestheaders(self, flow: mitmproxy.http.HTTPFlow):
           

(Called when) 來自用戶端的 HTTP 請求的頭部被成功讀取。此時 flow 中的 request 的 body 是空的。

def request(self, flow: mitmproxy.http.HTTPFlow):
           

(Called when) 來自用戶端的 HTTP 請求被成功完整讀取。

def responseheaders(self, flow: mitmproxy.http.HTTPFlow):
           

(Called when) 來自服務端的 HTTP 響應的頭部被成功讀取。此時 flow 中的 response 的 body 是空的。

def response(self, flow: mitmproxy.http.HTTPFlow):
           

(Called when) 來自服務端端的 HTTP 響應被成功完整讀取。

def error(self, flow: mitmproxy.http.HTTPFlow):
           

(Called when) 發生了一個 HTTP 錯誤。比如無效的服務端響應、連接配接斷開等。注意與“有效的 HTTP 錯誤傳回”不是一回事,後者是一個正确的服務端響應,隻是 HTTP code 表示錯誤而已。

2. 針對 TCP 生命周期

def tcp_start(self, flow: mitmproxy.tcp.TCPFlow):
           

(Called when) 建立了一個 TCP 連接配接。

def tcp_message(self, flow: mitmproxy.tcp.TCPFlow):
           

(Called when) TCP 連接配接收到了一條消息,最近一條消息存于 flow.messages[-1]。消息是可修改的。

def tcp_error(self, flow: mitmproxy.tcp.TCPFlow):
           

(Called when) 發生了 TCP 錯誤。

def tcp_end(self, flow: mitmproxy.tcp.TCPFlow):
           

(Called when) TCP 連接配接關閉。

3. 針對 Websocket 生命周期

def websocket_handshake(self, flow: mitmproxy.http.HTTPFlow):
           

(Called when) 用戶端試圖建立一個 websocket 連接配接。可以通過控制 HTTP 頭部中針對 websocket 的條目來改變握手行為。flow 的 request 屬性保證是非空的的。

def websocket_start(self, flow: mitmproxy.websocket.WebSocketFlow):
           

(Called when) 建立了一個 websocket 連接配接。

def websocket_message(self, flow: mitmproxy.websocket.WebSocketFlow):
           

(Called when) 收到一條來自用戶端或服務端的 websocket 消息。最近一條消息存于 flow.messages[-1]。消息是可修改的。目前有兩種消息類型,對應 BINARY 類型的 frame 或 TEXT 類型的 frame。

def websocket_error(self, flow: mitmproxy.websocket.WebSocketFlow):
           

(Called when) 發生了 websocket 錯誤。

def websocket_end(self, flow: mitmproxy.websocket.WebSocketFlow):
           

(Called when) websocket 連接配接關閉。

4. 針對網絡連接配接生命周期

def clientconnect(self, layer: mitmproxy.proxy.protocol.Layer):
           

(Called when) 用戶端連接配接到了 mitmproxy。注意一條連接配接可能對應多個 HTTP 請求。

def clientdisconnect(self, layer: mitmproxy.proxy.protocol.Layer):
           

(Called when) 用戶端斷開了和 mitmproxy 的連接配接。

def serverconnect(self, conn: mitmproxy.connections.ServerConnection):
           

(Called when) mitmproxy 連接配接到了服務端。注意一條連接配接可能對應多個 HTTP 請求。

def serverdisconnect(self, conn: mitmproxy.connections.ServerConnection):
           

(Called when) mitmproxy 斷開了和服務端的連接配接。

def next_layer(self, layer: mitmproxy.proxy.protocol.Layer):
           

(Called when) 網絡 layer 發生切換。你可以通過傳回一個新的 layer 對象來改變将被使用的 layer。詳見 layer 的定義。

5. 通用生命周期

def configure(self, updated: typing.Set[str]):
           

(Called when) 配置發生變化。updated 參數是一個類似集合的對象,包含了所有變化了的選項。在 mitmproxy 啟動時,該事件也會觸發,且 updated 包含所有選項。

def done(self):
           

(Called when) addon 關閉或被移除,又或者 mitmproxy 本身關閉。由于會先等事件循環終止後再觸發該事件,是以這是一個 addon 可以看見的最後一個事件。由于此時 log 也已經關閉,是以此時調用 log 函數沒有任何輸出。

def load(self, entry: mitmproxy.addonmanager.Loader):
           

(Called when) addon 第一次加載時。entry 參數是一個 Loader 對象,包含有添加選項、指令的方法。這裡是 addon 配置它自己的地方。

def log(self, entry: mitmproxy.log.LogEntry):
           

(Called when) 通過 mitmproxy.ctx.log 産生了一條新日志。小心不要在這個事件内打日志,否則會造成死循環。

def running(self):
           

(Called when) mitmproxy 完全啟動并開始運作。此時,mitmproxy 已經綁定了端口,所有的 addon 都被加載了。

def update(self, flows: typing.Sequence[mitmproxy.flow.Flow]):
           

(Called when) 一個或多個 flow 對象被修改了,通常是來自一個不同的 addon。

Reference:

  • mitmproxy 官方文檔:https://docs.mitmproxy.org/stable
  • mitmproxy 腳本示例:https://github.com/mitmproxy/mitmproxy/tree/master/examples

使我有洛陽二傾田,安能配六國相印

繼續閱讀