mitmproxy是一个支持HTTP和HTTPS的抓包程序,类似Fiddler、Charles的功能,只不过它通过控制台的形式操作。
此外,mitmproxy还有两个关联组件,一个是mitmdump,它是mitmproxy的命令行接口,利用它可以对接Python脚本,实现监听后的处理;另一个是mitmweb,它是一个Web程序,通过它以清楚地观察到mitmproxy捕获的请求。
linux下借助pip,可以一键安装:pip install mitmproxy
windows 下需要安装Microsoft Visual C++ V14.0以上,之后再使用pip install mitmproxy安装
mitmproxy有三大组件:
在windows下仅支持后两种组件的使用。
本节中,我们就来了解一下mitmproxy、mitmdump和mitmweb的安装方式。
最简单的安装方式还是使用pip,直接执行如下命令即可安装:
pip3 install mitmproxy
这是最简单和通用的安装方式,执行完毕之后即可完成mitmproxy的安装,另外还附带安装了mitmdump和mitmweb这两个组件。如果不想用这种方式安装,也可以选择后面列出的专门针对各个平台的安装方式或者Docker安装方式。
可以到GitHub上的Releases页面(链接为:https://github.com/mitmproxy/mitmproxy/releases/)获取安装包,如图1-59所示。
比如,当前的最新版本为2.0.2,则可以选择下载Windows下的exe安装包mitmproxy-2.0.2-windows-installer.exe,下载后直接双击安装包即可安装。
注意,在Windows上不支持mitmproxy的控制台接口,但是可以使用mitmdump和mitmweb。
在Linux下,可以下载编译好的二进制包(下载地址https://github.com/mitmproxy/mitmproxy/releases/),此发行包一般是最新版本,它包含了最新版本的mitmproxy和内置的Python 3环境,以及最新的OpenSSL环境。
如果你的环境里没有Python 3和OpenSSL环境,建议使用此种方式安装。
下载之后,需要解压并将其配置到环境变量:
tar -zxvf mitmproxy-2.0.2-linux.tar.gz
sudo mv mitmproxy mitmdump mitmweb /usr/bin
这样就可以将3个可执行文件移动到了/usr/bin目录。而一般情况下,/usr/bin目录都已经配置在了环境变量下,所以接下来可以直接调用这3个工具了。
Mac下的安装非常简单,直接使用Homebrew即可,命令如下:
brew install mitmproxy
执行命令后,即可完成mitmproxy的安装。
mitmproxy也支持Docker,其DockerHub的地址为https://hub.docker.com/r/mitmproxy/mitmproxy/。
在Docker下,mitmproxy的安装命令为:
docker run --rm -it -p 8080:8080 mitmproxy/mitmproxy mitmdump
这样就在8080端口上启动了mitmproxy和mitmdump。
如果想要获取CA证书,可以选择挂载磁盘选项,命令如下:
docker run --rm -it -v ~/.mitmproxy:/home/mitmproxy/.mitmproxy -p 8080:8080 mitmproxy/mitmproxy mitmdump
这样就可以在~/.mitmproxy目录下找到CA证书。
另外,还可以在8081端口上启动mitmweb,命令如下:
docker run --rm -it -p 8080:8080 -p 127.0.0.1:8081:8081 mitmproxy/mitmproxy mitmweb
更多启动方式可以参考Docker Hub的安装说明。
对于mitmproxy来说,如果想要截获HTTPS请求,就需要设置证书。mitmproxy在安装后会提供一套CA证书,只要客户端信任了mitmproxy提供的证书,就可以通过mitmproxy获取HTTPS请求的具体内容,否则mitmproxy是无法解析HTTPS请求的。
首先,运行以下命令产生CA证书,并启动mitmdump:
mitmdump
接下来,我们就可以在用户目录下的.mitmproxy目录里面找到CA证书,如图1-60所示。
证书一共5个,表1-1简要说明了这5个证书。
名称 | 描述 |
---|---|
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安全性 |
下面我们介绍一下Windows、Mac、iOS和Android平台下的证书配置过程。
双击mitmproxy-ca.p12,就会出现导入证书的引导页,如图1-61所示。
直接点击“下一步”按钮即可,会出现密码设置提示,如图1-62所示。
这里不需要设置密码,直接点击“下一步”按钮即可。
接下来需要选择证书的存储区域,如图1-63所示。这里点击第二个选项“将所有的证书都放入下列存储”,然后点击“浏览”按钮,选择证书存储位置为“受信任的根证书颁发机构”,接着点击“确定”按钮,然后点击“下一步”按钮。
最后,如果有安全警告弹出,如图1-64所示,直接点击“是”按钮即可。
这样就在Windows下配置完CA证书了。
Mac下双击mitmproxy-ca-cert.pem即可弹出钥匙串管理页面,然后找到mitmproxy证书,打开其设置选项,选择“始终信任”即可,如图1-65所示。
在linux下启动 mitmproxy,命令为mitmproxy -p 8080,同时将手机代理设置为linux的IP地址与端口后访问mitm.it安装证书。
mitmweb # mitmproxy有三种启动方式,此处使用的命令可以提供一个web交互界面
【提示】
mitmproxy有三种启动命令:
(1) mitmweb
– 提供一个web界面;
– 代理端口:绑定了 *:8080作为代理端口;
– 交互界面地址:localhost:8081;
(2) mitmproxy
– 提供命令行界面;
– 可以通过命令过滤请求;
(3) mitmdump
mitmdump 命令启动后,没有界面,程序默默运行,所以 mitmdump 无法提供过滤请求、查看数据的功能,只能结合自定义脚本,默默工作。
得到下图,表示启动成功,端口号默认为 8080。
mitmproxy服务器绑定了 *:8080作为代理端口
此时,浏览器自动打开下图页面,这是 mitmproxy 提供的 web 交互界面。
上图为mitmproxy自动打开的web交互界面
第一步,将电脑和手机连到同一个 WiFi 中;
第二步,获取本机的内网IP地址;
ifconfig
得到电脑本机的内网IP地址为 192.168.1.102。
第三步,给手机WiFi配置代理
第四步,安装证书
使用手机浏览器访问mitm.it,得到下图。
我的手机为iPhone,点击 Apple 后得到下图。
点击允许,开始安装。
安装完成后,得到已验证的提示。
手机依次点击:设置 -> 通用 -> 关于本机 -> 证书信任设置,开启 mitmproxy 证书。
注意:
- 有小伙伴反馈,安卓 7.0 以上版本不再信任证书,需要将证书安装到 root 路径下,这导致数据抓包不成功、手机应用网络不通。
- 博主还没有做过研究,想提示一下安卓 7.0 用户如果遇到这类问题,可以尝试使用模拟器降低安卓版本,看是否能解决问题,有时间的话麻烦把结果给博主反馈一下,谢谢!
此时,mitmweb 页面出现下图内容,红框中的为 mitmproxy 抓取的手机的请求。
到这里,就成功完成了 mitmproxy配置。
将mitmproxy-ca-cert.pem文件发送到iPhone上,推荐使用邮件方式发送,然后在iPhone上可以直接点击附件并识别安装,如图1-66所示。
点击“安装”按钮之后,会跳到安装描述文件的页面,点击“安装”按钮,此时会有警告提示,如图1-67所示。
继续点击右上角的“安装”按钮,安装成功之后会有已安装的提示,如图1-68所示。
如果你的iOS版本是10.3以下的话,此处信任CA证书的流程就已经完成了。
如果你的iOS版本是10.3及以上版本,还需要在“设置”→“通用”→“关于本机”→“证书信任设置”将mitmproxy的完全信任开关打开,如图1-69所示。此时,在iOS上配置信任CA证书的流程就结束了。
在Android手机上,同样需要将证书mitmproxy-ca-cert.pem文件发送到手机上,例如直接复制文件。
接下来,点击证书,便会出现一个提示窗口,如图1-70所示。
这时输入证书的名称,然后点击“确定”按钮即可完成安装。
mitmproxy过滤功能的使用举例:
输入z,清除屏幕上全部的包
输入f,进入编辑模式,可在最下面编辑条件,ESC或Enter退出编辑
!(~c 200) #显示所有返回不是200的请求
!(~c 200) & ~d baidu.com #显示域名包含baidu.com,返回不是200的请求
~m post & ~u baidu #显示请求的链接里面包含baidu的post请求
~d baidu.com (http://baidu.com) 过滤所有域名包含baidu.com (http://baidu.com)的包
mitmproxy断点功能的使用举例:
输入i,进入编辑模式,可在最下面编辑条件,ESC或Enter退出编辑
断点的条件和过滤是一样的,符合条件的链接会被拦截
~d baidu.com & ~m get
域名包含baidu.com的get 请求会被拦截
按Enter进入详情页,在详情页输入e进入模式,可以修改各项数据
完成后,回到请求显示列表,输入a,将请求放行.
1.请求重放
2.选中需要重放的请求,输入r可以重放请求,也可以先编辑后再重放
3.输入Q可退出程序
编写一个 py 文件供 mitmproxy 加载,文件定义了变量 addons,addons 是个数组,每个元素是一个类实例,这些类有若干方法,这些方法实现了某些 mitmproxy 提供的事件,mitmproxy 会在某个事件发生时调用对应的方法。这些类,称为一个个 addon,比如一个叫 Counter 的 addons:
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 的实现方式。
启动 mitmproxy:
mitmweb --listen-port 8080 -s addons.py
当浏览器使用代理进行访问时,就应该能看到控制台里有类似这样的日志。
# mitmdump -p [port] -s [脚本文件]
$ mitmdump -q -s addons.py --set body-size-limit=10k "~m post" -w outfile
先导入ctx模块:from mitmproxy import ctx。这个模块主要用于日志的输出。日志输出可以控制颜色
ctx.log.info # 日志输出颜色为白色
ctx.log.warn # 日志输出颜色为黄色
ctx.log.error # 日志输出颜色为红色
上述的脚本估计不用我解释相信大家也看明白了,就是当 request 发生时,计数器加一,并打印日志。这里对应的是 request 事件
事件针对不同生命周期分为 5 类。“生命周期”这里指在哪一个层面看待事件,举例来说,同样是一次 web 请求,我可以理解为**“HTTP 请求 -> HTTP 响应”的过程,也可以理解为“TCP 连接 -> TCP 通信 -> TCP 断开”**的过程。那么,如果我想拒绝来个某个 IP 的客户端请求,应当注册函数到针对 TCP 生命周期 的 tcp_start 事件,又或者,我想阻断对某个特定域名的请求时,则应当注册函数到针对 HTTP 声明周期的 http_connect 事件。其他情况同理。
下面一段估计会又臭又长,如果你没有耐心看完,那至少看掉针对 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 表示错误而已。
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 连接关闭。
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 连接关闭。
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 的定义。
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。
估计看了那么多的事件你已经晕了,正常,鬼才会记得那么多事件。事实上考虑到 mitmproxy 的实际使用场景,大多数情况下我们只会用到针对 HTTP 生命周期的几个事件。再精简一点,甚至只需要用到 http_connect、request、response 三个事件就能完成大多数需求了。
这里以一个稍微有点黑色幽默的例子,覆盖这三个事件,展示如果利用 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)
为了实现第四个需求,我们需要将代码整理一下,即易于管理也易于查看。
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