Mitmproxy/mitmdump中文文檔以及配置教程
https://ptorch.com/news/269.html
https://ptorch.com/docs/10/mitmproxy_introduction
本文是一個較為完整的mitmproxy
教程,側重於介紹如何開發攔截腳本,幫助讀者能夠快速得到一個自定義的代理工具。
本文假設讀者有基本的python
知識,且已經安裝好了一個python 3
開發環境。如果你對 nodejs 的熟悉程度大於對 python
,可移步到 anyproxy,anyproxy
的功能與mitmproxy
基本一致,但使用js
編寫定制腳本。除此之外我就不知道有什么其他類似的工具了,如果你知道,歡迎評論告訴我。
本文基於mitmproxy v5
,當前版本號為 v5.0.1。
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 來說似乎不大常用,故本文僅討論正向代理模式。
2 Features
- 攔截HTTP和HTTPS請求和響應並即時修改它們
- 保存完整的HTTP對話以供以之后重發和分析
- 重發HTTP對話的客戶端
- 重發先前記錄的服務的HTTP響應
- 反向代理模式將流量轉發到指定的服務器
- 在macOS和Linux上實現透明代理模式
- 使用Python對HTTP流量進行腳本化修改
- 實時生成用於攔截的SSL / TLS證書
- And much, much more…
3 Installation
“安裝 mitmproxy”這句話是有歧義的,既可以指“安裝 mitmproxy 工具”,也可以指“安裝 python 的 mitmproxy 包”,注意后者是包含前者的。
如果只是拿 mitmproxy 做一個替代 fiddler 的工具,沒有什么定制化的需求,那完全只需要“安裝 mitmproxy 工具”即可,去 mitmproxy 官網 上下載一個 installer 便可開箱即用,不需要提前准備好 python 開發環境。但顯然,這不是這里要討論的,我們需要的是“安裝 python 的 mitmproxy 包”。
安裝 python 的 mitmproxy 包除了會得到 mitmproxy 工具外,還會得到開發定制腳本所需要的包依賴,其安裝過程並不復雜。
首先需要安裝好 python,版本需要不低於 3.6,且安裝了附帶的包管理工具 pip。這里不做展開,假設你已經准備好這樣的環境了。
安裝開始。
在 linux 中:
pip3.8 install mitmproxy
在 windows 中,以管理員身份運行 cmd 或 power shell:
pip3.8 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
4 Run
要啟動 mitmproxy 用 mitmproxy
、mitmdump
、mitmweb
這三個命令中的任意一個即可,這三個命令功能一致,且都可以加載自定義腳本,唯一的區別是交互界面的不同。
mitmproxy
命令啟動后,會提供一個命令行界面,用戶可以實時看到發生的請求,並通過命令過濾請求,查看請求數據。形如:
配置代理的方法與配置burpsuite一樣,我用的谷歌瀏覽器(127.0.0.1 8080),然后命令行執行
mitmproxy --listen-host 127.0.0.1 -p 8080
之后,瀏覽器地址訪問http://mitm.it/
,點擊Other下載安裝證書,下圖所示:
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 交互來過濾請求,查看請求數據。形如:
5 Scripts
5.1 編寫HTTP/1.1和HTTP/2.0腳本
完成了上述工作,我們已經具備了操作 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()
]
from mitmproxy import ctx
# 必須這么寫
def request(flow):
print(flow.request.headers)
ctx.log.info(str(flow.request.headers))
ctx.log.warn(str(flow.request.headers))
ctx.log.error(str(flow.request.headers))
# http.HTTPFlow 實例 flow
flow.request.headers # 獲取所有頭信息,包含Host、User-Agent、Content-type等字段
flow.request.url # 完整的請求地址,包含域名及請求參數,但是不包含放在body里面的請求參數
flow.request.pretty_url # 同flow.request.url目前沒看出什么差別
flow.request.host # 域名
flow.request.method # 請求方式。POST、GET等
flow.request.scheme # 什么請求 ,如https
flow.request.path # 請求的路徑,url除域名之外的內容
flow.request.get_text() # 請求中body內容,有一些http會把請求參數放在body里面,那么可通過此方法獲取,返回字典類型
flow.request.query # 返回MultiDictView類型的數據,url直接帶的鍵值參數
flow.request.get_content() # bytes,結果如flow.request.get_text()
flow.request.raw_content # bytes,結果如flow.request.get_content()
flow.request.urlencoded_form # MultiDictView,content-type:application/x-www-form-urlencoded時的請求參數,不包含url直接帶的鍵值參數
flow.request.multipart_form #MultiDictView,content-type:multipart/form-data
def response(flow):
flow.response.status_code # 狀態碼
flow.response.text # 返回內容,已解碼
flow.response.content # 返回內容,二進制
flow.response.setText() # 修改返回內容,不需要轉碼
這里強烈建議使用第二種套路,直覺上就會感覺第二種套路更為先進,使用會更方便也更容易管理和拓展。況且這也是官方內置的一些 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
……
這就說明自定義腳本生效了。
5.2 腳本化WebSocket
在客戶端和服務器同意將連接升級到WebSocket之前,WebSocket協議最初看起來像是常規HTTP請求。初始HTTP握手的所有腳本事件以及專用的WebSocket事件都可以在此處找到。
"""Process individual messages from a WebSocket connection."""
import re
from mitmproxy import ctx
def websocket_message(flow):
# get the latest message
message = flow.messages[-1]
# was the message sent from the client or server?
if message.from_client:
ctx.log.info("Client sent a message: {}".format(message.content))
else:
ctx.log.info("Server sent a message: {}".format(message.content))
# manipulate the message content
message.content = re.sub(r'^Hello', 'HAPPY', message.content)
if 'FOOBAR' in message.content:
# kill the message and not send it to the other endpoint
message.kill()
對於與WebSocket相關的對象,請查看websocket模塊以查找在編寫腳本時可以使用的所有屬性。
5.3 編寫TCP腳本
可以在此處找到有關TCP協議的所有事件。
"""
Process individual messages from a TCP connection.
This script replaces full occurences of "foo" with "bar" and prints various details for each message.
Please note that TCP is stream-based and *not* message-based. mitmproxy splits stream contents into "messages"
as they are received by socket.recv(). This is pretty arbitrary and should not be relied on.
However, it is sometimes good enough as a quick hack.
Example Invocation:
mitmdump --rawtcp --tcp-hosts ".*" -s examples/tcp-simple.py
"""
from mitmproxy.utils import strutils
from mitmproxy import ctx
from mitmproxy import tcp
def tcp_message(flow: tcp.TCPFlow):
message = flow.messages[-1]
message.content = message.content.replace(b"foo", b"bar")
ctx.log.info(
f"tcp_message[from_client={message.from_client}), content={strutils.bytes_to_escaped_str(message.content)}]"
)
對於與WebSocket相關的對象,請查看tcp模塊以查找在編寫腳本時可以使用的所有屬性。
6 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。