1. 程式人生 > >使用 mitmproxy + python 做攔截代理

使用 mitmproxy + python 做攔截代理

http proxy 在web滲透上佔據著非常重要的地位,這方面的工具也非常多,像burp suite, Fiddler,Charles簡直每個搞web的必備神器。本文主要介紹 mitmproxy,是一個較為完整的 mitmproxy 教程,側重於介紹如何開發攔截指令碼,幫助讀者能夠快速得到一個自定義的代理工具。

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

本文基於 mitmproxy v4,當前版本號為 v4.0.1

mitmproxy 是什麼

顧名思義,mitmproxy 就是用於 MITM 的 proxy,MITM 即 中間人攻擊(Man-in-the-middle attack),mitmproxy 譯為中間人代理工具,可以用來攔截、修改、儲存 HTTP/HTTPS 請求。以命令列終端形式呈現,操作上類似於Vim,同時提供了 mitmweb 外掛,是類似於 Chrome 瀏覽器開發者模式的視覺化工具。中間人代理一般在客戶端和伺服器之間的網路中攔截、監聽和篡改資料。用於中間人攻擊的代理首先會向正常的代理一樣轉發請求,保障服務端與客戶端的通訊,其次,會適時的查、記錄其截獲的資料,或篡改資料,引發服務端或客戶端特定的行為。

不同於 fiddler 或 wireshark 等抓包工具,mitmproxy 不僅可以截獲請求幫助開發者檢視、分析,更可以通過自定義指令碼進行二次開發。舉例來說,利用 fiddler 可以過濾出瀏覽器對某個特定 url 的請求,並檢視、分析其資料,但實現不了高度定製化的需求,類似於:“截獲對瀏覽器對該 url 的請求,將返回內容置空,並將真實的返回內容存到某個資料庫,出現異常時發出郵件通知”。mitmproxy 它是基於Python開發的開源工具,最重要的是它提供了Python API,這樣就可以通過載入自定義 python 指令碼輕鬆實現使用Python程式碼來控制請求和響應。這是其它工具所不能做到的。

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

那這樣的工具有什麼實際意義呢?據我所知目前比較廣泛的應用是做模擬爬蟲,即利用手機模擬器、無頭瀏覽器來爬取 APP 或網站的資料,mitmpproxy 作為代理可以攔截、儲存爬蟲獲取到的資料,或修改資料調整爬蟲的行為。

事實上,以上說的僅是 mitmproxy 以正向代理模式工作的情況,通過調整配置,mitmproxy 還可以作為透明代理、反向代理、上游代理、SOCKS 代理等,

總共有五種代理模式:

  • 1、正向代理(regular proxy)啟動時預設選擇的模式。是一個位於客戶端和原始伺服器(origin server)之間的伺服器,為了從原始伺服器取得內容,客戶端向mitmproxy代理髮送一個請求並指定目標(原始伺服器),然後代理向原始伺服器轉交請求並將獲得的內容返回給客戶端。客戶端必須要進行一些特別的設定才能使用正向代理。
  • 2、反向代理(reverse proxy)啟動引數 -R host。跟正向代理正好相反,對於客戶端而言它就像是原始伺服器,並且客戶端不需要進行任何特別的設定。客戶端向mitmproxy代理伺服器傳送普通請求,mitmproxy轉發請求到指定的伺服器,並將獲得的內容返回給客戶端,就像這些內容 原本就是它自己的一樣。
  • 3、上行代理(upstream proxy)啟動引數 -U host。mitmproxy接受代理請求,並將所有請求無條件轉發到指定的上游代理伺服器。這與反向代理相反,其中mitmproxy將普通HTTP請求轉發給上游伺服器。
  • 4、透明代理(transparent proxy)啟動引數 -T。當使用透明代理時,流量將被重定向到網路層的代理,而不需要任何客戶端配置。這使得透明代理非常適合那些無法更改客戶端行為的情況 - 代理無聊的Android應用程式是一個常見的例子。要設定透明代理,我們需要兩個新的元件。第一個是重定向機制,可以將目的地為Internet上的伺服器的TCP連線透明地重新路由到偵聽代理伺服器。這通常採用與代理伺服器相同的主機上的防火牆形式。比如Linux下的iptables, 或者OSX中的pf,一旦客戶端初始化了連線,它將作出一個普通的HTTP請求(注意,這種請求就是客戶端不知道代理存在)請求頭中沒有scheme(比如http://或者https://), 也沒有主機名(比如example.com)我們如何知道上游的主機是哪個呢?路由機制執行了重定向,但保持了原始的目的地址。
    iptable設定:
            iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
            iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8080
    啟用透明代理:mitmproxy -T
  • 5、socks5 proxy 啟動引數 --socks。採用socks協議的代理伺服器

但這些工作模式針對 mitmproxy 來說似乎不大常用,故本文僅討論正向代理模式。

mitmproxy是一款Python語言開發的開源中間人代理神器,支援SSL,支援透明代理、反向代理,支援流量錄製回放,支援自定義指令碼等。功能上同Windows中的Fiddler有些類似,但mitmproxy是一款console程式,沒有GUI介面,不過用起來還算方便。使用mitmproxy可以很方便的過濾、攔截、修改任意經過代理的HTTP請求/響應資料包,甚至可以利用它的scripting API,編寫指令碼達到自動攔截修改HTTP資料的目的。

# test.py
def response(flow):
    flow.response.headers["BOOM"] = "boom!boom!boom!"

上面的指令碼會在所有經過代理的Http響應包頭裡面加上一個名為BOOM的header。用mitmproxy -s 'test.py'命令啟動mitmproxy,curl驗證結果發現的確多了一個BOOM頭。

$ http_proxy=localhost:8080 curl -I 'httpbin.org/get'
HTTP/1.1 200 OK
Server: nginx
Date: Thu, 03 Nov 2016 09:02:04 GMT
Content-Type: application/json
Content-Length: 186
Connection: keep-alive
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
BOOM: boom!boom!boom!
...

mitmweb 抓包截圖:

顯然 mitmproxy 指令碼能做的事情遠不止這些,結合Python強大的功能,可以衍生出很多應用途徑。除此之外,mitmproxy還提供了強大的API,在這些API的基礎上,完全可以自己定製一個實現了特殊功能的專屬代理伺服器。

經過效能測試,發現 mitmproxy 的效率並不是特別高。如果只是用於除錯目的那還好,但如果要用到生產環境,有大量併發請求通過代理的時候,效能還是稍微差點。

工作原理

mitmproxy 實現原理:

  1. 客戶端發起一個到 mitmproxy 的連線,並且發出 HTTP CONNECT 請求,
  2. mitmproxy 作出響應 (200),模擬已經建立了 CONNECT 通訊管道,
  3. 客戶端確信它正在和遠端伺服器會話,然後啟動 SSL 連線。在 SSL 連線中指明瞭它正在連線的主機名 (SNI),
  4. mitmproxy 連線伺服器,然後使用客戶端發出的 SNI 指示的主機名建立 SSL 連線,
  5. 伺服器以匹配的 SSL 證書作出響應,這個 SSL 證書裡包含生成的攔截證書所必須的通用名 (CN) 和伺服器備用名 (SAN),
  6. mitmproxy 生成攔截證書,然後繼續進行與第 3 步暫停的客戶端 SSL 握手,
  7. 客戶端通過已經建立的 SSL 連線傳送請求,
  8. mitmproxy 通過第 4 步建立的 SSL 連線傳遞這個請求給伺服器。

mitmproxy 工作步驟:

  1. 設定系統、瀏覽器、終端等的代理地址和埠為同一區域網中 mitmproxy 所在電腦的 IP 地址,比如我的 PC 開啟 mitmproxy 之後,設定 8080 埠,本地 IP 為 192.168.1.130,那麼設定 Android HTTP 代理為 192.168.1.130:8080
  2. 瀏覽器或移動端訪問 mitm.it 來安裝 mitmproxy 提供的證書
  3. 在 mitmproxy 提供的命令列下,或者 mitmweb 提供的瀏覽器介面中就能看到 Android 端發出的請求。

安裝

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

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

安裝 python 的 mitmproxy 包除了會得到 mitmproxy 工具外,還會得到開發定製指令碼所需要的包依賴,其安裝過程並不複雜。

首先需要安裝好 python,版本需要不低於 3.6,且安裝了附帶的包管理工具 pip。不同作業系統安裝 python 3 的方式不一,參考 python 的下載頁,這裡不做展開,假設你已經準備好這樣的環境了。

在 linux 中sudo pip3 install mitmproxy

一旦使用者安裝上了mitmproxy,那麼,在python的dist-packages目錄下就會有一個libmproxy的目錄。點選進去,如下圖所示。

有很多檔案,裡面最關鍵的一個檔案就是flow.py。裡面有從客戶端請求的類Request,也有從伺服器返回的可以操作的類Response。並且都實現了一些方法可以呼叫請求或回覆的資料,包括請求url,header,body,content等。具體如下:

Request的一些方法:

get_query()      :得到請求的url的引數,被存放成了字典。
set_query(odict) :設定請求的url引數,引數是字典。
get_url()        :請求的url。
set_url(url)     :設定url的域。
get_cookies()    :得到請求的cookie。
headers          :請求的header的字典。
content          :請求的內容,如果請求時post,那麼content就是指代post的引數。

Response的一些方法如下:

Headers     :返回的header的字典。
Code        :返回資料包的狀態,比如200,301之類的狀態。
Httpversion :http版本。

在 windows 中,以管理員身份執行 cmd 或 power shell:pip3 install mitmproxy

完成後,系統將擁有 mitmproxy、mitmdump、mitmweb 三個命令,由於 mitmproxy 命令不支援在 windows 系統中執行(這沒關係,不用擔心),

我們可以拿 mitmdump 測試一下安裝是否成功,執行:mitmdump --version

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

Mitmproxy: 4.0.1
Python:    3.6.5
OpenSSL:   OpenSSL 1.1.0h  27 Mar 2018
Platform:  Windows-10-10.0.16299-SP0

安裝 CA 證書

方法1:

對於mitmproxy 來說,如果想要截獲HTTPS請求,就得解決證書認證的問題,就需要設定CA證書,因此需要在通訊發生的客戶端安裝證書,並且設定為受信任的根證書頒佈機構。而mitmproxy安裝後就會提供一套CA證書,只要客戶信任了此證書即可。
當我們初次執行 mitmproxy 或 mitmdump 時,會在當前目錄下生成 ~/.mitmproxy資料夾,其中該檔案下包含4個檔案,這就是我們要的證書了。
localhost:app zhangtao$ mitmdump
Proxy server listening at http://*:8080

檔案說明:

  • mitmproxy-ca.pem PEM格式的證書私鑰
  • mitmproxy-ca-cert.pem PEM格式證書,適用於大多數非Windows平臺
  • mitmproxy-ca-cert.p12 PKCS12格式的證書,適用於大多數Windows平臺
  • mitmproxy-ca-cert.cer 與 mitmproxy-ca-cert.pem 相同(只是字尾名不同),適用於大部分Android平臺
  • mitmproxy-dhparam.pem PEM格式的祕鑰檔案,用於增強SSL安全性。

方法2:

配置 瀏覽器 和 手機 

  • 1.電腦和手機連線到同一個 wifi 環境下 
  • 2.修改瀏覽器代理伺服器地址為執行mitmproxy的那臺機器(本機)ip地址,埠設定為你啟動mitmproxy時設定的埠,如果沒有指定就使用8080 
  • 3.手機做同樣操作,修改wifi連結代理為 【手動】,然後指定ip地址和埠 

以手機配置為例: 

1. 設定伺服器、埠 

2 . 安裝 CA 證書 (只需要安裝一次證書即可)

第一次使用 mitmproxy 的時候需要安裝 CA 證書。在手機 或 pc 機上開啟瀏覽器訪問 這個地址,選擇你當前平臺的圖示,點選安裝證書。選擇你當前平臺的圖示,點選安裝證書。
在下圖中點選Apple安裝證書。

在各端配置好代理後,訪問:下載 CA 證書,並按照以下方式進行驗證。

iOS

  • 開啟設定-無線區域網-所連線的Wifi-配置代理-手動
  • 填上代理伺服器IP和埠
  • 開啟設定-通用-關於本機-證書信任設定
  • 開啟mitmproxy選項。

Android

  • 開啟設定-WLAN-長按所連線的網路-修改網路-高階選項-手動
  • 填入代理伺服器IP和埠
  • 開啟設定-安全-信任的憑據
  • 檢視安裝的證書是否存在

macOS

  • 開啟系統配置(System Preferences.app)- 網路(Network)- 高階(Advanced)- 代理(Proxies)- Web Proxy(HTTP)和Secure Web Proxy(HTTPS)
  • 填上代理伺服器IP和埠
  • 開啟Keychain Access.app
  • 選擇login(Keychains)和Certificates(Category)中找到mitmproxy
  • 點選mitmproxy,在Trust中選擇Always Trust

執行啟動(啟動 mitmproxy 三種方式

在完成 mitmproxy 的安裝之後,mitm 提供的三個命令。要啟動 mitmproxy用 mitmproxy、mitmdump、mitmweb 這三個命令中的任意一個即可,這三個命令功能一致,且都可以載入自定義指令碼,唯一的區別是互動介面的不同。

  • mitmproxy 會提供一個在終端下的圖形介面,具有修改請求和響應,流量重放等功能,具體操作方式有點 vim 的風格
  • mitmdump 可設定規則儲存或重放請求和響應,mitmdump 的特點是支援 inline 指令碼,由於擁有可以修改 request 和 response 中每一個細節的能力,批量測試,劫持等都可以輕鬆實現
  • mitmweb 提供的一個簡單 web 介面,簡單實用,初學者或者對終端命令列不熟悉的可以用 mitmweb 介面

1. mitmproxy 直接啟動

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

mitmproxy 基本使用

可以使用 mitmproxy -h 來檢視 mitmproxy 的引數及使用方法。常用的幾個命令引數:

  • -p PORT, --port PORT 設定 mitmproxy 的代理埠
  • -T, --transparent 設定透明代理
  • --socks 設定 SOCKS5 代理
  • -s "script.py --bar", --script "script.py --bar" 來執行指令碼,通過雙引號來新增引數
  • -t FILTER 過濾引數

在 mitmproxy 命令模式下,在終端顯示請求流,可以通過 Shift + ? 來開啟幫助檢視當前頁面可用的命令。

基本快捷鍵

b  儲存請求 / 返回頭
C  將請求內容匯出到貼上板,按 C 之後會有選擇匯出哪一部分
d  刪除 flow 請求
E  將 flow 匯出到檔案
w  儲存所有 flow 或者該 flow
W  儲存該 flow
L  載入儲存的 Flow
m  新增 / 取消 Mark 標記,會在請求列表該請求前新增紅色圓圈
z  清空 flow list 和 eventlog
/  在詳情介面,可以使用 / 來搜尋,大小寫敏感
i  開啟 interception pattern 攔截請求

移動

j, k       上下
h, l        左右
g, G   go to beginning, end
space    下一頁
pg up/down   上一頁 / 下一頁
ctrl+b/ctrl+f    上一頁 / 下一頁
arrows 箭頭     上下左右


全域性快捷鍵
q   退出,或者後退
Q  不提示直接退出
  • mitmproxy的按鍵操作說明
按鍵 說明
q 退出(相當於返回鍵,可一級一級返回)
d 刪除當前(黃色箭頭)指向的連結
D 恢復剛才刪除的請求
G 跳到最新一個請求
g 跳到第一個請求
C 清空控制檯(C是大寫)
i 可輸入需要攔截的檔案或者域名(逗號需要用\來做轉譯,栗子:feezu.cn)
a 放行請求
A 放行所有請求
? 檢視介面幫助資訊
^ v 上下箭頭移動游標
enter 檢視游標所在列的內容
tab 分別檢視 Request 和 Response 的詳細資訊
/ 搜尋body裡的內容
esc 退出編輯
e 進入編輯模式

同樣在 mitmproxy 中不同介面中使用 ? 可以獲取不同的幫助,在請求詳細資訊中 m 快捷鍵的作用就完全不同 m 在響應結果中,輸入 m 可以選擇 body 的呈現方式,比如 json,xml 等 e 編輯請求、響應 a 傳送編輯後的請求、響應。 因此在熟悉使用 ? 之後,多次使用並熟悉快捷鍵即可。就如同在 Linux 下要熟悉使用 man 命令一樣,在不懂地方請教 Google 一樣,應該是習慣性動作。多次反覆之後就會變得非常數量。

2. mitmweb 命令啟動

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

3. mitmdump 命令啟動

mitmdump 命令啟動後——你應該猜到了,沒有介面,程式默默執行,所以 mitmdump 無法提供過濾請求、檢視資料的功能,只能結合自定義指令碼,默默工作。

4. 啟動示例

由於 mitmproxy 命令的互動操作稍顯繁雜且不支援 windows 系統,而我們主要的使用方式又是載入自定義指令碼,並不需要互動,所以原則上說只需要 mitmdump 即可,但考慮到有互動介面可以更方便排查錯誤,所以這裡以 mitmweb 命令為例。實際使用中可以根據情況選擇任何一個命令。

啟動 mitmproxy:mitmweb

應當看到如下輸出:

Web server listening at http://127.0.0.1:8081/
Proxy server listening at http://*:8080

mitmproxy 綁定了 *:8080 作為代理埠,並提供了一個 web 互動介面在 127.0.0.1:8081

現在可以測試一下代理,讓 Chrome 以 mitmproxy 為代理並忽略證書錯誤。為了不影響平時正常使用,我們不去改 Chrome 的配置,而是通過命令列帶引數起一個 Chrome。如果你不使用 Chrome 而是其他瀏覽器,也可以搜一下對應的啟動引數是什麼,應該不會有什麼坑。此外示例僅以 windows 系統為例,因為使用 linux 或 mac 開發的同學應該更熟悉命令列的使用才對,應當能自行推匯出在各自環境中對應的操作。

由於 Chrome 要開始赴湯蹈火走代理了,為了方便繼續在 web 介面上與 mitmproxy 互動,我們委屈求全使用 Edge 或其他瀏覽器開啟 127.0.0.1:8081。插一句,我用 Edge 實在是因為機器上沒其他瀏覽器了(IE 不算),Edge 有一個預設禁止訪問迴環地址的狗屁設定,詳見解決方案

接下來關閉所有 Chrome 視窗,否則命令列啟動時的附加引數將失效。開啟 cmd,執行:

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --proxy-server=127.0.0.1:8080 --ignore-certificate-errors

前面那一長串是 Chrome 的的安裝路徑,應當根據系統實際情況修改,後面兩引數設定了代理地址並強制忽略掉證書錯誤。用 Chrome 開啟一個網站,可以看到:

同時在 Edge 上可以看到:

指令碼

重點:一個完整的 HTTP flow 會依次觸發 requestheaders, request, responseheaders 和 response。

完成了上述工作,我們已經具備了操作 mitmproxy 的基本能力 了。接下來開始開發自定義指令碼,這才是 mitmproxy 真正強大的地方。使用 -s 引數 制定 inline 指令碼:

mitmproxy -s script.py

比如將指定 url 的請求指向新的地址

用於除錯 Android 或者 iOS 客戶端,打包比較複雜的時候,強行將客戶端請求從線上地址指向本地除錯地址。可以使用 mitmproxy scripting API mitmproxy 提供的事件驅動介面。

加上將線上地址,指向本地 8085 埠,檔案為 redirect_request.py

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
def request(flow):
    if flow.request.pretty_host == 'api.github.com':
        flow.request.host = '127.0.0.1'
        flow.request.port = 8085

則使用 mitmweb -s redirect_request.py 來呼叫此指令碼,則通過 mitm 的請求都會指向本地 http://127.0.0.1:8085。

更多的指令碼可以參考

啟用 SOCKS5 代理

新增引數 --socks 可以使用 mitmproxy 的 SOCK5 代理

透明代理

透明代理是指將網路流量直接重定向到網路埠,不需要客戶端做任何設定。這個特性使得透明代理非常適合不能對客戶端進行配置的時候,比如說 Android 應用等等。

指令碼編寫遵循的規定

指令碼的編寫需要遵循 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
……

這就說明自定義指令碼生效了。

---------------------------------------------------------------------------------------------------------------------------------

mitmproxy啟動時可以使用 -s 引數匯入外部的指令碼進行攔截處理

比如我要修改一個每個連結的響應頭的

python指令碼:

1、簡單方法

from mitmproxy import http
 
def response(flow: http.HTTPFlow) -> None:
    flow.response.headers["server"] = "nginx"

2、使用類

class ModifyHeader:
    def response(self, flow):
        flow.response.headers["serverr"] = "nginx"
 
 
def start():
    return ModifyHeader()

儲存為 modifyheader.py。然後啟動 mitmdump -s modifyheader.py,就會把代理抓到包的每個響應頭的Server都改成“nginx”

官方參考例子:https://github.com/mitmproxy/mitmproxy/tree/master/examples

事件

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

事件針對不同生命週期分為 5 類。“生命週期”這裡指在哪一個層面看待事件,舉例來說,同樣是一次 web 請求,我可以理解為“HTTP 請求 -> HTTP 響應”的過程,也可以理解為“TCP 連線 -> TCP 通訊 -> TCP 斷開”的過程。那麼,如果我想拒絕來個某個 IP 的客戶端請求,應當註冊函式到針對 TCP 生命週期 的 tcp_start 事件,又或者,我想阻斷對某個特定域名的請求時,則應當註冊函式到針對 HTTP 宣告週期的 http_connect 事件。其他情況同理。

下面一段估計會又臭又長,如果你沒有耐心看完,那至少看掉針對 HTTP 生命週期的事件,然後跳到示例

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。

主要 events 一覽表
   需要修改各種事件內容時,重寫以下對應方法,這裡主要用的是request、response方法

import typing

import mitmproxy.addonmanager
import mitmproxy.connections
import mitmproxy.http
import mitmproxy.log
import mitmproxy.tcp
import mitmproxy.websocket
import mitmproxy.proxy.protocol

def requestheaders(self, flow: mitmproxy.http.HTTPFlow):
    """
        HTTP request headers were successfully read. At this point, the body
        is empty.
    """

def request(self, flow: mitmproxy.http.HTTPFlow):
    """
        The full HTTP request has been read.
    """

def responseheaders(self, flow: mitmproxy.http.HTTPFlow):
    """
        HTTP response headers were successfully read. At this point, the body
        is empty.
    """

def response(self, flow: mitmproxy.http.HTTPFlow):
    """
        The full HTTP response has been read.
    """

def error(self, flow: mitmproxy.http.HTTPFlow):
    """
        An HTTP error has occurred, e.g. invalid server responses, or
        interrupted connections. This is distinct from a valid server HTTP
        error response, which is simply a response with an HTTP error code.
    """

# TCP lifecycle
def tcp_start(self, flow: mitmproxy.tcp.TCPFlow):
    """
        A TCP connection has started.
    """

def tcp_message(self, flow: mitmproxy.tcp.TCPFlow):
    """
        A TCP connection has received a message. The most recent message
        will be flow.messages[-1]. The message is user-modifiable.
    """

def tcp_error(self, flow: mitmproxy.tcp.TCPFlow):
    """
        A TCP error has occurred.
    """

def tcp_end(self, flow: mitmproxy.tcp.TCPFlow):
    """
        A TCP connection has ended.
    """

# Websocket lifecycle
def websocket_handshake(self, flow: mitmproxy.http.HTTPFlow):
    """
        Called when a client wants to establish a WebSocket connection. The
        WebSocket-specific headers can be manipulated to alter the
        handshake. The flow object is guaranteed to have a non-None request
        attribute.
    """

def websocket_start(self, flow: mitmproxy.websocket.WebSocketFlow):
    """
        A websocket connection has commenced.
    """

def websocket_message(self, flow: mitmproxy.websocket.WebSocketFlow):
    """
        Called when a WebSocket message is received from the client or
        server. The most recent message will be flow.messages[-1]. The
        message is user-modifiable. Currently there are two types of
        messages, corresponding to the BINARY and TEXT frame types.
    """

def websocket_error(self, flow: mitmproxy.websocket.WebSocketFlow):
    """
        A websocket connection has had an error.
    """

def websocket_end(self, flow: mitmproxy.websocket.WebSocketFlow):
    """
        A websocket connection has ended.
    """

# Network lifecycle
def clientconnect(self, layer: mitmproxy.proxy.protocol.Layer):
    """
        A client has connected to mitmproxy. Note that a connection can
        correspond to multiple HTTP requests.
    """

def clientdisconnect(self, layer: mitmproxy.proxy.protocol.Layer):
    """
        A client has disconnected from mitmproxy.
    """

def serverconnect(self, conn: mitmproxy.connections.ServerConnection):
    """
        Mitmproxy has connected to a server. Note that a connection can
        correspond to multiple requests.
    """

def serverdisconnect(self, conn: mitmproxy.connections.ServerConnection):
    """
        Mitmproxy has disconnected from a server.
    """

def next_layer(self, layer: mitmproxy.proxy.protocol.Layer):
    """
        Network layers are being switched. You may change which layer will
        be used by returning a new layer object from this event.
    """

# General lifecycle
def configure(self, updated: typing.Set[str]):
    """
        Called when configuration changes. The updated argument is a
        set-like object containing the keys of all changed options. This
        event is called during startup with all options in the updated set.
    """

def done(self):
    """
        Called when the addon shuts down, either by being removed from
        the mitmproxy instance, or when mitmproxy itself shuts down. On
        shutdown, this event is called after the event loop is
        terminated, guaranteeing that it will be the final event an addon
        sees. Note that log handlers are shut down at this point, so
        calls to log functions will produce no output.
    """

def load(self, entry: mitmproxy.addonmanager.Loader):
    """
        Called when an addon is first loaded. This event receives a Loader
        object, which contains methods for adding options and commands. This
        method is where the addon configures itself.
    """

def log(self, entry: mitmproxy.log.LogEntry):
    """
        Called whenever a new log entry is created through the mitmproxy
        context. Be careful not to log from this event, which will cause an
        infinite loop!
    """

def running(self):
    """
        Called when the proxy is completely up and running. At this point,
        you can expect the proxy to be bound to a port, and all addons to be
        loaded.
    """

def update(self, flows: typing.Sequence[mitmproxy.flow.Flow]):
    """
        Update is called when one or more flow objects have been modified,
        usually from a different addon.
    """

針對 http,常用的 API

# 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 時的請求引數,不包含url直接帶的鍵值引數

以上均為獲取 request 資訊的一些常用方法,對於 response,同理

flow.response.status_code  # 狀態碼
flow.response.text         # 返回內容,已解碼
flow.response.content      # 返回內容,二進位制
flow.response.setText()    # 修改返回內容,不需要轉碼

以上為不完全列舉

示例

修改response內容,這裡是伺服器已經有返回了結果,再更改,也可以做不經過伺服器處理,直接返回,看需求

def response(flow:http.HTTPFlow)-> None:
#特定介面需要返回1001結果
interface_list=["page/**"] #由於涉及公司隱私問題,隱藏實際的介面

url_path=flow.request.path
if  url_path.split("?")[0] in  interface_list:
    ctx.log.info("#"*50)
    ctx.log.info("待修改路徑的內容:"+url_path)
    ctx.log.info("修改成:1001錯誤返回")
    ctx.log.info("修改前:\n")
    ctx.log.info(flow.response.text)
    flow.response.set_text(json.dumps({"result":"1001","message":"服務異常"}))#修改,使用set_text不用轉碼
    ctx.log.info("修改後:\n")
    ctx.log.info(flow.response.text)
    ctx.log.info("#"*50)
elif  flow.request.host in  host_list:#host_list 域名列表,作為全域性變數,公司有多個域名,也隱藏
    ctx.log.info("response= "+flow.response.text)

示 例

估計看了那麼多的事件你已經暈了,正常,鬼才會記得那麼多事件。事實上考慮到 mitmproxy 的實際使用場景,大多數情況下我們只會用到針對 HTTP 生命週期的幾個事件。再精簡一點,甚至只需要用到 http_connectrequestresponse 三個事件就能完成大多數需求了。

這裡以一個稍微有點黑色幽默的例子,覆蓋這三個事件,展示如果利用 mitmproxy 工作。

需求是這樣的:

  1. 因為百度搜索是不靠譜的,所有當客戶端發起百度搜索時,記錄下使用者的搜尋詞,再修改請求,將搜尋詞改為“360 搜尋”;
  2. 因為 360 搜尋還是不靠譜的,所有當客戶端訪問 360 搜尋時,將頁面中所有“搜尋”字樣改為“請使用谷歌”。
  3. 因為谷歌是個不存在的網站,所有就不要浪費時間去嘗試連線服務端了,所有當發現客戶端試圖訪問谷歌時,直接斷開連線。
  4. 將上述功能組裝成名為 Joker 的 addon,並保留之前展示名為 Counter 的 addon,都載入進 mitmproxy。

第一個需求需要篡改客戶端請求,所以實現一個 request 事件:

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"))
    # 替換搜尋詞為“360搜尋”
    flow.request.query.set_all("wd", ["360搜尋"])

第二個需求需要篡改服務端響應,所以實現一個 response 事件:

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)

第三個需求需要拒絕客戶端請求,所以實現一個 http_connect 事件:

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)

為了實現第四個需求,我們需要將程式碼整理一下,即易於管理也易於檢視。

建立一個 joker.py 檔案,內容為:

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)

建立一個 counter.py 檔案,內容為:

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.py 檔案,內容為:

import counter
import joker

addons = [
    counter.Counter(),
    joker.Joker(),
]

將三個檔案放在相同的資料夾,在該資料夾內啟動命令列,執行:mitmweb -s addons.py

老規矩,關閉所有 Chrome 視窗,從命令列中啟動 Chrome 並指定代理且忽略證書錯誤。

測試一下執行效果:

指令碼編寫

編寫指令碼的話,主要用到的有兩個東西
    - Event
    - API

1. Event 事件
        事件裡面有3個事件是比較重要的
                start 啟動的時候被呼叫,會替換當前的外掛, 可以用此事件註冊過濾器.
                request(flow) 當傳送請求時,被呼叫.
                response(flow) 當接收到回覆時被呼叫.

2. API
        三個比較重要的資料結構
                mitmproxy.models.http.HTTPRequest
                mitmproxy.models.http.HTTPResponse
                mitmproxy.models.http.HTTPFlow

先上程式碼:頭腦王者即時顯示答案指令碼

#!/usr/bin/env python
#coding=utf-8
import sys
import json
from mitmproxy import flowfilter
from pymongo import MongoClient
reload(sys)
sys.setdefaultencoding('utf-8')

'''
頭腦王者即時顯示答案指令碼
'''

class TNWZ:
    '''
    從抓包可以看到 問題包的連結最後是 findQuiz
    '''
    def __init__(self):
        #新增一個過濾器,只處理問題包
        self.filter = flowfilter.parse('~u findQuiz')
       #連線答案資料庫
        self.conn = MongoClient('localhost', 27017)
        self.db = self.conn.tnwz
        self.answer_set = self.db.quizzes

    def request(self, flow):
        '''
        演示request事件效果, 請求的時候輸出提示
        :param flow: 
        :return: 
        '''
        if flowfilter.match(self.filter,flow):

            print(u'準備請求答案')

    def responseheaders(self, flow):
         '''
        演示responseheaders事件效果, 新增頭資訊
        :param flow: 
        :return: 
        '''
        if flowfilter.match(self.filter, flow):
            flow.response.headers['Cache-Control'] = 'no-cache'
            flow.response.headers['Pragma'] = 'no-cache'

    def response(self, flow):
        '''
        HTTPEvent 下面所有事件引數都是 flow 型別 HTTPFlow
        可以在API下面查到 HTTPFlow, 下面有一個屬性response 型別 TTPResponse
        HTTPResponse 有個屬性為 content 就是response在內容,更多屬性可以檢視 文件
        :param flow: 
        :return: 
        '''

        if flowfilter.match(self.filter, flow):
            #匹配上後證明抓到的是問題了, 查答案
            data = flow.response.content
            quiz = json.loads(data)
            #獲取問題
            question = quiz['quiz']
            print(question)

            #獲取答案
            answer = self.answer_set.find_one({"quiz":question})
            if answer is None:
                print('no answer')
            else:
                answerIndex = int(answer['answer'])-1
                options = answer['options']
                print(options[answerIndex])

#這裡簡單演示下start事件
def start():
    return TNWZ()

使用方法:mitmdump -s quiz.py

啟動後也可以編輯指令碼檔案,mitmdump會自動重新載入不需要重新執行命令,本來是寫了一個頭腦王者自動抓問題找答案,結果遊戲被封了,只在本地模擬下

是不是很簡單, 如果還不夠,可以考慮,在傳送答案的時候, 攔截請求,然後替換為標準答案再發送到伺服器,是不是很給力。工具都是死的,就看怎麼用了。

環境搭建

appium

程式碼 start_appium.py

# -*- coding: utf-8 -*-
# @Time    : 2018/10/8 11:00
# @Author  : cxa
# @File    : test.py
# @Software: PyCharmctx
from appium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import time
import base64


def start_appium():
    desired_caps = {}
    desired_caps['platformName'] = 'Android'  # 裝置系統
    desired_caps['deviceName'] = '127.0.0.1:62001'  # 裝置名稱
    desired_caps['appPackage'] = 'com.xxxx.xxxx'  # 測試app包名,如何獲取包名方式看上面的環境搭建。
    desired_caps['appActivity'] = 'com.xxxx.xxxx.xxx.xxxx'  # 測試appActivity,如何獲取包名方式看上面的環境搭建。
    desired_caps['platformVersion'] = '4.4.2'  # 裝置系統的安卓版本,版本不要太高,設計安全策略得外部因素。
    desired_caps['noReset'] = True  # 啟動後結束後不清空應用資料
    desired_caps['unicodeKeyboard'] = True  # 此兩行是為了解決字元輸入不正確的問題
    desired_caps['resetKeyboard'] = True  # 執行完成後重置軟鍵盤的狀態  
    driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)  # 啟動app,啟動前記得開啟appium服務。
    wait = WebDriverWait(driver, 60)#設定等待事件
    try:
        btn_xpath = '//android.widget.Button[@resource-id="com.alicom.smartdail:id/m_nonum_confirm_btn"]'
        btn_node = wait.until(EC.presence_of_element_located((By.XPATH, btn_xpath)))#等元素出現再繼續,最長等待時間上面設定的60s。
        # btn_node=driver.find_element_by_xpath(btn_xpath)
        btn_node.click()
    except:
        driver.back()
        btn_xpath = '//android.widget.Button[@resource-id="com.alicom.smartdail:id/m_nonum_confirm_btn"]'
        btn_node = wait.until(EC.presence_of_element_located((By.XPATH, btn_xpath)))
        # btn_node = driver.find_element_by_xpath(btn_xpath)
        btn_node.click()
    # sleep 30s
    # 點選


def login_in(driver):
    id_xpath = '//android.widget.EditText[@content-desc="賬戶名輸入框"]'
    id_node = driver.find_element_by_xpath(id_xpath)
    id_node.clear()
    id_node.send_keys("test")
    pwd = str(base64.b64decode("MTIzNHF3ZXI="), 'u8')
    pwd_xpath = '//android.widget.EditText[@content-desc="密碼輸入框"]'
    pwd_node = driver.find_element_by_xpath(pwd_xpath)
    pwd_node.clear()
    pwd_node.send_keys(pwd)
    submit = "//android.widget.Button[@text='登入']"
    submit_node = driver.find_element_by_xpath(submit)
    submit_node.click()
    time.sleep(10)


if __name__ == '__main__':
    start_appium()

mitmproxy

程式碼 mitm_proxy_script.py

# -*- coding: utf-8 -*-
# @Time    : 2018/10/8 11:00
# @Author  : cxa
# @File    : mitm_proxy_script.py
# @Software: PyCharm
import sys
sitename = 'ali'


def response(flow):
    request = flow.request
    if '.png' in request.url or 'xxx.x.xxx.com' not in request.url:
        return  #如果不在觀察的url內則返回
    if 'xxx.x.xxx.com' in request .url:
        print(request .url)
        cookies = dict(request.cookies) #轉換cookies格式為dict
        if cookies:
            save_cookies(repr(cookies))#如果不為空儲存cookies


def save_cookies(cookies):
    sys.path.append("../")
    from database import getcookies
    getcookies.insert_data(sitename, cookies) #儲存cookies