1豫柬、顧名思義告希,mitmproxy 就是用于 MITM 的 proxy,MITM 即[中間人攻擊]烧给,用于中間人攻擊的代理首先會向正常的代理一樣轉(zhuǎn)發(fā)請求暂雹,保障服務端與客戶端的通信,其次创夜,會適時的查杭跪、記錄其截獲的數(shù)據(jù),或篡改數(shù)據(jù)驰吓,引發(fā)服務端或客戶端特定的行為涧尿。
2、不同于 fiddler 或 wireshark 等抓包工具檬贰,mitmproxy 不僅可以截獲請求幫助開發(fā)者查看姑廉、分析,更可以通過自定義腳本進行二次開發(fā)翁涤。舉例來說桥言,利用 fiddler 可以過濾出瀏覽器對某個特定 url 的請求,并查看葵礼、分析其數(shù)據(jù)号阿,但實現(xiàn)不了高度定制化的需求,類似于:“截獲對瀏覽器對該 url 的請求鸳粉,將返回內(nèi)容置空扔涧,并將真實的返回內(nèi)容存到某個數(shù)據(jù)庫,出現(xiàn)異常時發(fā)出郵件通知”。而對于 mitmproxy枯夜,這樣的需求可以通過載入自定義 python 腳本輕松實現(xiàn)弯汰。
3、但 mitmproxy 并不會真的對無辜的人發(fā)起中間人攻擊湖雹,由于 mitmproxy 工作在 HTTP 層咏闪,而當前 HTTPS 的普及讓客戶端擁有了檢測并規(guī)避中間人攻擊的能力,所以要讓 mitmproxy 能夠正常工作摔吏,必須要讓客戶端(APP 或瀏覽器)主動信任 mitmproxy 的 SSL 證書鸽嫂,或忽略證書異常,這也就意味著 APP 或瀏覽器是屬于開發(fā)者本人的——顯而易見舔腾,這不是在做黑產(chǎn)溪胶,而是在做開發(fā)或測試搂擦。
4稳诚、那這樣的工具有什么實際意義呢?據(jù)我所知目前比較廣泛的應用是做仿真爬蟲瀑踢,即利用手機模擬器扳还、無頭瀏覽器來爬取 APP 或網(wǎng)站的數(shù)據(jù),mitmproxy 作為代理可以攔截橱夭、存儲爬蟲獲取到的數(shù)據(jù)氨距,或修改數(shù)據(jù)調(diào)整爬蟲的行為。
事實上棘劣,以上說的僅是 mitmproxy 以正向代理模式工作的情況俏让,通過調(diào)整配置,mitmproxy 還可以作為透明代理茬暇、反向代理首昔、上游代理、SOCKS 代理等糙俗,但這些工作模式針對 mitmproxy 來說似乎不大常用勒奇,故本文僅討論正向代理模式。
5巧骚、python腳本不要小于3.6
6赊颠、安裝完后,mitmdump 是命令行工具,mitmweb是一個web界面劈彪。
7竣蹦、第一個套路是,編寫一個 py 文件供 mitmproxy 加載沧奴,文件中定義了若干函數(shù)草添,這些函數(shù)實現(xiàn)了某些 mitmproxy 提供的事件,mitmproxy 會在某個事件發(fā)生時調(diào)用對應的函數(shù)扼仲,形如:
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 是個數(shù)組驰后,每個元素是一個類實例肆资,這些類有若干方法,這些方法實現(xiàn)了某些 mitmproxy 提供的事件灶芝,mitmproxy 會在某個事件發(fā)生時調(diào)用對應的方法郑原。這些類,稱為一個個 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()
]
以上面的腳本啟動
mitmweb -s addons.py
8犯犁、事件針對不同生命周期分為 5 類∨鳎“生命周期”這里指在哪一個層面看待事件酸役,舉例來說,同樣是一次 web 請求驾胆,我可以理解為“HTTP 請求 -> HTTP 響應”的過程涣澡,也可以理解為“TCP 連接 -> TCP 通信 -> TCP 斷開”的過程。那么丧诺,如果我想拒絕來個某個 IP 的客戶端請求入桂,應當注冊函數(shù)到針對 TCP 生命周期 的 tcp_start 事件,又或者驳阎,我想阻斷對某個特定域名的請求時抗愁,則應當注冊函數(shù)到針對 HTTP 聲明周期的 http_connect 事件。其他情況同理
9呵晚、def http_connect(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 收到了來自客戶端的 HTTP CONNECT 請求蜘腌。在 flow 上設置非 2xx 響應將返回該響應并斷開連接。CONNECT 不是常用的 HTTP 請求方法劣纲,目的是與服務器建立代理連接逢捺,僅是 client 與 proxy 的之間的交流,所以 CONNECT 請求不會觸發(fā) request癞季、response 等其他常規(guī)的 HTTP 事件劫瞳。
10、def requestheaders(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 來自客戶端的 HTTP 請求的頭部被成功讀取绷柒。此時 flow 中的 request 的 body 是空的志于。
11、def request(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 來自客戶端的 HTTP 請求被成功完整讀取废睦。
12伺绽、def responseheaders(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 來自服務端的 HTTP 響應的頭部被成功讀取。此時 flow 中的 response 的 body 是空的。
13奈应、def response(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 來自服務端端的 HTTP 響應被成功完整讀取澜掩。
14、def error(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 發(fā)生了一個 HTTP 錯誤杖挣。比如無效的服務端響應肩榕、連接斷開等。注意與“有效的 HTTP 錯誤返回”不是一回事惩妇,后者是一個正確的服務端響應株汉,只是 HTTP code 表示錯誤而已。
15歌殃、將百度搜索替換成360搜索:
def request(self, flow: mitmproxy.http.HTTPFlow):
# 忽略非百度搜索地址
if flow.request.host != "www.baidu.com" or not flow.request.path.startswith("/s"):
return
# 確認請求參數(shù)中有搜索詞
if "wd" not in flow.request.query.keys():
ctx.log.warn("can not get search word from %s" % flow.request.pretty_url)
return
# 輸出原始的搜索詞
ctx.log.info("catch search word: %s" % flow.request.query.get("wd"))
# 替換搜索詞為“360搜索”
flow.request.query.set_all("wd", ["360搜索"])
16乔妈、360搜索改成谷歌搜索
def response(self, flow: mitmproxy.http.HTTPFlow):
# 忽略非 360 搜索地址
if flow.request.host != "www.so.com":
return
# 將響應中所有“搜索”替換為“請使用谷歌”
text = flow.response.get_text()
text = text.replace("搜索", "請使用谷歌")
flow.response.set_text(text)
17、如果客戶想訪問谷歌氓皱,拒絕:
def http_connect(self, flow: mitmproxy.http.HTTPFlow):
# 確認客戶端是想訪問 www.google.com
if flow.request.host == "www.google.com":
# 返回一個非 2xx 響應斷開連接
flow.response = http.HTTPResponse.make(404)
18路召、整合:
import mitmproxy.http
from mitmproxy import ctx, http
class Joker:
def request(self, flow: mitmproxy.http.HTTPFlow):
if flow.request.host != "www.baidu.com" or not flow.request.path.startswith("/s"):
return
if "wd" not in flow.request.query.keys():
ctx.log.warn("can not get search word from %s" % flow.request.pretty_url)
return
ctx.log.info("catch search word: %s" % flow.request.query.get("wd"))
flow.request.query.set_all("wd", ["360搜索"])
def response(self, flow: mitmproxy.http.HTTPFlow):
if flow.request.host != "www.so.com":
return
text = flow.response.get_text()
text = text.replace("搜索", "請使用谷歌")
flow.response.set_text(text)
def http_connect(self, flow: mitmproxy.http.HTTPFlow):
if flow.request.host == "www.google.com":
flow.response = http.HTTPResponse.make(404)
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)
import counter
import joker
addons = [
counter.Counter(),
joker.Joker(),
]
mitmweb -s addons.py
18、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) 發(fā)生了 TCP 錯誤。
def tcp_end(self, flow: mitmproxy.tcp.TCPFlow):
(Called when) TCP 連接關閉抡医。
19躲因、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) 發(fā)生了 websocket 錯誤宙刘。
def websocket_end(self, flow: mitmproxy.websocket.WebSocketFlow):
(Called when) websocket 連接關閉苍姜。
20、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) 網(wǎng)絡 layer 發(fā)生切換。你可以通過返回一個新的 layer 對象來改變將被使用的 layer棵譬。
21铁追、
def configure(self, updated: typing.Set[str]):
(Called when) 配置發(fā)生變化。updated 參數(shù)是一個類似集合的對象茫船,包含了所有變化了的選項琅束。在 mitmproxy 啟動時,該事件也會觸發(fā),且 updated 包含所有選項。
def done(self):
(Called when) addon 關閉或被移除培愁,又或者 mitmproxy 本身關閉培廓。由于會先等事件循環(huán)終止后再觸發(fā)該事件,所以這是一個 addon 可以看見的最后一個事件疗我。由于此時 log 也已經(jīng)關閉,所以此時調(diào)用 log 函數(shù)沒有任何輸出。
def load(self, entry: mitmproxy.addonmanager.Loader):
(Called when) addon 第一次加載時屿岂。entry 參數(shù)是一個 Loader 對象,包含有添加選項鲸匿、命令的方法爷怀。這里是 addon 配置它自己的地方。
def log(self, entry: mitmproxy.log.LogEntry):
(Called when) 通過 mitmproxy.ctx.log 產(chǎn)生了一條新日志带欢。小心不要在這個事件內(nèi)打日志运授,否則會造成死循環(huán)。
def running(self):
(Called when) mitmproxy 完全啟動并開始運行乔煞。此時吁朦,mitmproxy 已經(jīng)綁定了端口,所有的 addon 都被加載了渡贾。
def update(self, flows: typing.Sequence[mitmproxy.flow.Flow]):
(Called when) 一個或多個 flow 對象被修改了逗宜,通常是來自一個不同的 addon。