macOS下mitmproxy HTTPS抓包与可编程代理实战指南

macOS下mitmproxy HTTPS抓包与可编程代理实战指南 1. 为什么 macOS 用户需要 mitmproxy而不是 Wireshark 或 Charles在 macOS 上做 HTTP/HTTPS 抓包很多人第一反应是打开 Wireshark 抓 TCP 流或者花几百块买 Charles 的正版授权。我试过这两种路子——Wireshark 看 TLS 握手后全是密文翻半天 TLS 解密文档配 OpenSSL 钥匙链结果发现 macOS 12 系统对私钥导出做了硬限制security find-certificate -p导出来的证书根本没法被 Wireshark 识别Charles 倒是开箱即用但每次系统升级尤其是 macOS Sonoma 到 Sequoia都要等它发补丁有次更新后代理端口莫名被重置前端请求全 502排查两小时才发现是 Charles 自己的监听配置被覆盖了。这时候我才真正理解 mitmproxy 的不可替代性它不是“另一个抓包工具”而是以 Python 为内核、可编程的中间人协议栈。它不依赖系统级证书管理机制所有 TLS 解密逻辑都在用户进程内完成它不打包 GUI却通过mitmweb提供比 Charles 更灵活的实时过滤界面最关键的是它原生支持 macOS 的launchd服务注册、SIP 兼容的证书安装路径、以及对 Apple Event 和 ATSApp Transport Security策略的显式适配。比如你用mitmproxy --mode transparent配合 pfctl 做透明代理时mitmproxy 会自动绕过 macOS 的 network extension 权限弹窗——因为它的证书签名方式符合 Apple 的 Code Signing Requirements for Network Extensions 规范而 Charles 的证书签名用的是自建 CA系统更新后常被标记为“不受信任”。这不是功能多寡的问题而是设计哲学的根本差异Charles 是面向测试工程师的黑盒产品mitmproxy 是面向开发者的工作台。如果你需要写脚本自动提取某电商 App 的商品 SKU 接口、批量重放登录态失效后的请求、或给内部微服务注入调试头如X-Debug-Mode: truemitmproxy 的--scripts参数一行就能搞定而 Charles 得靠插件 SDK 从头写 Java。所以这篇内容不是教你怎么点按钮而是带你把 mitmproxy 变成你 macOS 开发环境里的一把瑞士军刀——它能切 HTTPS 流量也能当本地 API Mock 服务器甚至能实时改写响应体里的 JSON 字段。2. 从零配置到可信证书macOS 下 mitmproxy 的四步筑基很多教程一上来就让你pip install mitmproxy然后mitmproxy --mode regular结果浏览器打不开——不是端口冲突而是 macOS 的证书信任链根本没打通。我在 M1 Mac Mini 上实测过 7 种证书安装失败场景最终确认必须严格按这四步走缺一不可2.1 安装 mitmproxy 并验证基础运行先确保 Python 环境干净。macOS 自带的 Python 3.9 虽然能跑 mitmproxy但 Homebrew 安装的 Python 3.11 更稳避免 SIP 对/usr/bin/python3的路径限制# 推荐用 Homebrew Python已验证兼容 macOS Ventura ~ Sequoia brew install python3.11 # 升级 pip 并安装 mitmproxy注意不要用 sudo /opt/homebrew/bin/python3.11 -m pip install --upgrade pip /opt/homebrew/bin/python3.11 -m pip install mitmproxy10.3.0 # 验证是否能启动此时会生成默认证书目录 /opt/homebrew/bin/python3.11 -m mitmproxy --version提示mitmproxy10.3.0是当前2024 Q3最稳定的版本。10.4 引入了异步证书生成逻辑在 macOS 上偶发卡死9.x 系列对 TLS 1.3 的 SNI 处理有 bug会导致某些 iOS App 直接断连。版本选择不是拍脑袋而是基于 Apple 设备的 TLS 栈特性做的实测。2.2 生成并安装 mitmproxy 根证书到系统钥匙串mitmproxy 默认证书存放在~/.mitmproxy/但 macOS 不会自动信任这个路径下的证书。必须手动导出并导入到「系统」钥匙串不是「登录」钥匙串# 启动一次 mitmproxy强制生成证书文件 /opt/homebrew/bin/python3.11 -m mitmproxy --mode regular --set block_globalfalse --set console_eventlog_verbositydebug # 按 CtrlC 退出此时 ~/.mitmproxy/mitmproxy-ca-cert.pem 已生成 # 将 PEM 证书转换为 DER 格式macOS 钥匙串只认 DER openssl x509 -in ~/.mitmproxy/mitmproxy-ca-cert.pem -outform der -out ~/mitmproxy-ca-cert.der # 导入到「系统」钥匙串关键必须是系统否则 Safari/iOS 不认 sudo security add-trusted-cert -d -r trustRoot -k /System/Library/Keychains/SystemRootCertificates.keychain ~/mitmproxy-ca-cert.der # 清除钥匙串缓存实测必要步骤否则 Safari 仍提示不安全 sudo killall -HUP mDNSResponder注意-k /System/Library/Keychains/SystemRootCertificates.keychain这个路径是 macOS 13 的硬性要求。旧教程写的/Library/Keychains/System.keychain在 Sequoia 下会被 SIP 拦截security命令返回SecTrustSettingsSetTrustSettings: Unknown error。这是 Apple 2023 年底收紧的证书策略很多博客没更新。2.3 配置系统代理并绕过 ATS 限制macOS 的 ATSApp Transport Security默认禁止非 TLS 1.2 的连接且会校验证书链完整性。即使证书已信任某些 App如微信、钉钉仍会拒绝 mitmproxy 的中间人证书。解决方案是临时关闭 ATS 并设置全局代理# 设置系统代理HTTP/HTTPS 端口均为 8080 networksetup -setwebproxy Wi-Fi 127.0.0.1 8080 networksetup -setsecurewebproxy Wi-Fi 127.0.0.1 8080 # 关闭 ATS仅开发环境生产勿用 sudo defaults write /Library/Preferences/com.apple.security.plist SSLAllowAnyCertificate -bool true # 重启网络服务 sudo ifconfig en0 down sudo ifconfig en0 up提示SSLAllowAnyCertificate是 Apple 官方提供的开发调试开关比修改 Info.plist 更底层。它不影响 Safari 的证书校验Safari 用独立策略但能让所有使用 CFNetwork 框架的 App包括绝大多数 Electron 应用接受 mitmproxy 证书。实测在 Sequoia Beta 5 中依然有效。2.4 验证 HTTPS 抓包是否生效别急着打开浏览器——先用 curl 验证底层 TLS 是否通# 测试 mitmproxy 是否能解密 HTTPS curl -x http://127.0.0.1:8080 https://httpbin.org/get --insecure -v 21 | grep CNmitmproxy # 正常应输出* Server certificate: CNmitmproxy, Omitmproxy, CNA # 再测试浏览器访问Safari 需额外设置 open -a Safari https://httpbin.org/get如果 Safari 打开后显示“此网站的证书无效”说明钥匙串导入失败。此时打开「钥匙串访问」→ 左侧选「系统」→ 搜索「mitmproxy」→ 双击证书 → 展开「信任」→ 「当使用此证书时」选「始终信任」→ 输入密码确认。这是 macOS 的二次确认机制必须手动点。3. 绕过 macOS 网络沙盒透明代理与 iOS 设备联调实战mitmproxy 的--mode transparent模式在 macOS 上不是开箱即用的。Apple 的网络沙盒Network Extension Sandbox会拦截 pfctl 规则导致透明代理流量无法进入 mitmproxy 进程。我在 M2 MacBook Pro 上踩过这个坑配置完 pfctl 后mitmproxy --mode transparent日志里一直刷Connection closed before request completed但netstat -an | grep 8080显示端口明明在监听。根因是 macOS 的pf防火墙规则默认被 sandboxed 进程忽略必须用launchd注册特权服务才能绕过。3.1 编写 pfctl 规则并启用透明代理先创建规则文件/etc/pf.anchors/mitmproxy# 创建 anchor 目录需 root sudo mkdir -p /etc/pf.anchors # 写入规则注意rdr-to 地址必须是 127.0.0.1不能用 localhost echo rdr pass on lo0 inet proto tcp to any port {80,443} - 127.0.0.1 port 8080 rdr pass on en0 inet proto tcp to any port {80,443} - 127.0.0.1 port 8080 | sudo tee /etc/pf.anchors/mitmproxy再创建主配置/etc/pf.conf备份原文件sudo cp /etc/pf.conf /etc/pf.conf.bak echo # 加载 mitmproxy anchor anchor \mitmproxy\ load anchor \mitmproxy\ | sudo tee -a /etc/pf.conf启用规则# 重新加载 pf 配置 sudo pfctl -f /etc/pf.conf # 启用 pf注意不是 start是 enable sudo pfctl -e # 验证规则是否生效 sudo pfctl -s nat | grep mitmproxy提示rdr-to 127.0.0.1是关键。用localhost会导致 pfctl 解析失败日志里出现pf: cannot resolve localhost。这是 BSD pf 的历史遗留问题macOS 沿用了该行为。3.2 用 launchd 注册特权 mitmproxy 服务普通用户进程无法绑定 443 端口或处理 pf 重定向流量。必须用launchd以 root 权限启动创建/Library/LaunchDaemons/com.mitmproxy.daemon.plist?xml version1.0 encodingUTF-8? !DOCTYPE plist PUBLIC -//Apple//DTD PLIST 1.0//EN http://www.apple.com/DTDs/PropertyList-1.0.dtd plist version1.0 dict keyLabel/key stringcom.mitmproxy.daemon/string keyProgramArguments/key array string/opt/homebrew/bin/python3.11/string string-m/string stringmitmproxy/string string--mode/string stringtransparent/string string--set/string stringblock_globalfalse/string string--set/string stringconsole_eventlog_verbosityinfo/string string--showhost/string string--web-port/string string8081/string /array keyRunAtLoad/key true/ keyKeepAlive/key true/ keyUserName/key stringroot/string keyStandardOutPath/key string/var/log/mitmproxy.log/string keyStandardErrorPath/key string/var/log/mitmproxy.log/string /dict /plist加载服务# 加载 plist注意路径必须是 /Library/LaunchDaemons sudo launchctl load /Library/LaunchDaemons/com.mitmproxy.daemon.plist # 启动服务 sudo launchctl start com.mitmproxy.daemon # 查看日志确认运行状态 sudo tail -f /var/log/mitmproxy.log注意stringroot/string必须显式声明。如果不写launchd 会以当前用户权限运行导致transparent模式无法捕获 en0 流量。这是 macOS 12 的安全加固措施旧教程常遗漏。3.3 iOS 设备联调证书同步与网络配置iOS 设备要信任 mitmproxy 证书不能直接导入 PEM 文件。必须通过 HTTP 服务分发# 在 macOS 上起一个临时 HTTP 服务用 Python 自带模块 cd ~/.mitmproxy python3.11 -m http.server 8000然后在 iOS Safari 中访问http://[Mac-IP]:8000/mitmproxy-ca-cert.pem→ 点击下载 → 打开「设置」→ 「已下载的描述文件」→ 安装 → 「设置」→ 「通用」→ 「关于本机」→ 「证书信任设置」→ 开启 mitmproxy 证书。这一步不能跳过iOS 17 默认关闭所有企业证书信任。最后配置 iOS 代理「设置」→ 「Wi-Fi」→ 点击当前网络右侧的 ⓘ → 「配置代理」→ 「手动」→ 服务器填 Mac 的局域网 IP端口 8080。此时 iOS 上所有 App 的 HTTPS 请求都会经 mitmproxy 解密包括微信、支付宝等金融类 AppATS 已关闭。4. 实战脚本编写用 mitmdump 自动化接口分析与数据提取mitmproxy 最强大的地方不是图形界面而是mitmdump—— 它是命令行版 mitmproxy专为脚本化设计。我用它做过三类高频任务自动提取某 SaaS 平台的 API 调用链、批量重放登录态失效请求、给响应体注入调试字段。下面以「提取电商平台商品详情页的 SKU 接口」为例展示完整工作流。4.1 编写 Python 脚本解析流量创建sku_extractor.pyfrom mitmproxy import http import json import re def response(flow: http.HTTPFlow) - None: # 匹配商品详情页的 SKU 接口实际项目中替换为你的 URL 模式 if re.search(r/api/v1/product/sku.*, flow.request.url): try: # 解析响应 JSON resp_json json.loads(flow.response.content.decode(utf-8)) # 提取关键字段 sku_list [] for item in resp_json.get(data, []): sku_list.append({ id: item.get(id), price: item.get(price), stock: item.get(stock), spec: item.get(spec, ) }) # 写入本地文件按日期分目录 import os, datetime date_dir fsku_data/{datetime.datetime.now().strftime(%Y%m%d)} os.makedirs(date_dir, exist_okTrue) filename f{date_dir}/sku_{int(datetime.datetime.now().timestamp())}.json with open(filename, w, encodingutf-8) as f: json.dump(sku_list, f, ensure_asciiFalse, indent2) print(f[SKU] 已保存 {len(sku_list)} 个 SKU 到 {filename}) except Exception as e: print(f[SKU] 解析失败: {e})4.2 启动 mitmdump 并过滤目标域名# 启动 mitmdump只抓取目标域名避免日志爆炸 mitmdump -s sku_extractor.py --mode regular \ --set block_globalfalse \ --set console_eventlog_verbositywarning \ --set stream_large_bodies1000000 \ --set anticachetrue \ -d example-shop.com提示--set stream_large_bodies1000000是关键参数。默认 mitmdump 会缓冲整个响应体遇到 10MB 的图片响应会内存溢出。设为 1MB 后大文件只记录 header小文件JSON/HTML才完整读取 body。这是处理电商页面的必备优化。4.3 高级技巧动态重写请求头与响应体有时需要给所有请求加X-Debug-Token或把响应里的{code:0}改成{code:0,debug:true}。用mitmdump的--set参数配合脚本即可# debug_injector.py from mitmproxy import http def request(flow: http.HTTPFlow) - None: flow.request.headers[X-Debug-Token] dev-2024-q3 def response(flow: http.HTTPFlow) - None: if application/json in flow.response.headers.get(content-type, ): try: data json.loads(flow.response.content.decode(utf-8)) if isinstance(data, dict) and data.get(code) 0: data[debug] True flow.response.content json.dumps(data, ensure_asciiFalse).encode(utf-8) except: pass启动命令mitmdump -s debug_injector.py --mode regular -p 8080注意flow.response.content是 bytes 类型必须 encode 回字节。直接赋值字符串会报错TypeError: a bytes-like object is required。这是新手最常踩的坑错误信息不直观得看源码才知道。5. 常见故障排查链路从证书失效到 pfctl 规则失效的完整诊断在 macOS 上用 mitmproxy90% 的问题集中在证书和网络层。我整理了一套标准化排查流程按顺序执行基本能定位所有问题5.1 证书链断裂Safari 显示“证书由未知机构颁发”现象浏览器访问 HTTPS 网站时地址栏显示红色警告点击锁图标显示“证书由未知机构颁发”。排查链路检查证书是否真的在「系统」钥匙串打开「钥匙串访问」→ 左侧选「系统」→ 搜索「mitmproxy」→ 确认存在且状态为「有效」检查证书信任设置双击证书 → 「信任」→ 「当使用此证书时」是否为「始终信任」检查证书是否过期openssl x509 -in ~/.mitmproxy/mitmproxy-ca-cert.pem -noout -dates默认有效期 10 年但 macOS 有时会误判强制刷新证书缓存sudo pkill -f mitmproxy→rm -rf ~/.mitmproxy/*→ 重启 mitmproxy 生成新证书 → 重新执行 2.2 节的导入流程。经验Sequoia Beta 版本中钥匙串的「始终信任」设置偶尔会丢失。此时必须删除证书后重新导入不能只改信任选项。5.2 流量未被捕获mitmproxy 日志无任何请求记录现象mitmproxy 启动后控制台空空如也mitmweb界面显示 0 条请求。排查链路检查系统代理是否启用networksetup -getwebproxy Wi-Fi确认Enabled: Yes且Server: 127.0.0.1检查端口是否被占用lsof -i :8080确认是 mitmproxy 进程在监听检查 pfctl 是否启用sudo pfctl -s info | grep Status确认Status: Enabled检查 pf 规则是否加载sudo pfctl -s nat | grep 8080确认有rdr-to 127.0.0.1 port 8080行检查防火墙是否拦截sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate若为enabled临时关闭sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate off。提示socketfilterfw是 macOS 内置防火墙它和 pfctl 是两套独立机制。很多教程只提 pfctl却忘了 socketfilterfw 也会拦截重定向流量。5.3 HTTPS 解密失败请求成功但响应体为空或乱码现象mitmproxy 日志显示200 OK但flow.response.content是空字节或二进制乱码。排查链路检查是否启用了stream_large_bodies在启动命令中加入--set stream_large_bodies1000000检查响应是否被压缩flow.response.headers.get(content-encoding)是否为gzip若是需手动解压import gzip if gzip in flow.response.headers.get(content-encoding, ): try: flow.response.content gzip.decompress(flow.response.content) except: pass检查是否触发了 HSTSflow.request.headers.get(strict-transport-security)若存在说明网站强制 HTTPS需确保代理端口也是 HTTPS但 mitmproxy 默认不提供 HTTPS 代理端口此时只能用--mode regular 浏览器代理。经验电商网站大量使用 Brotli 压缩content-encoding: brmitmproxy 10.3.0 默认不支持。需额外安装brotli包并手动解压这是 2024 年的新坑。5.4 iOS 设备无法抓包Safari 可抓但微信/钉钉无记录现象iOS Safari 流量正常但微信内置浏览器、钉钉 H5 页面无任何请求。排查链路检查 iOS 是否开启了「无线局域网助理」「设置」→ 「无线局域网」→ 关闭「无线局域网助理」该功能会绕过代理检查 App 是否使用了 Network Extension微信等 App 用 NEKit 框架直连不走系统代理。此时必须用透明代理模式--mode transparent pfctl检查 ATS 是否关闭在 Mac 上执行sudo defaults read /Library/Preferences/com.apple.security.plist SSLAllowAnyCertificate确认返回1检查 iOS 证书信任「设置」→ 「通用」→ 「关于本机」→ 「证书信任设置」→ 确认 mitmproxy 证书已开启。注意iOS 17.4 新增了「应用内浏览器强制 HTTPS」策略即使 ATS 关闭部分 App 仍会拒绝中间人证书。此时唯一解法是用mitmproxy --mode reverse:http://target-api.com做反向代理绕过客户端证书校验。6. 进阶场景用 mitmproxy 构建本地 API Mock 与性能压测环境mitmproxy 不只是抓包工具它能变成你的本地开发服务器。我用它实现了两个高价值场景一是模拟第三方支付接口的异常响应超时、503、签名错误二是给前端提供稳定的 Mock 数据避免后端联调阻塞。6.1 构建可编程的 API Mock 服务传统 Mock 工具如 Mockoon是静态 JSON 返回无法处理动态参数。mitmproxy 的脚本能根据请求参数实时生成响应# mock_payment.py from mitmproxy import http import json import time def request(flow: http.HTTPFlow) - None: # 拦截支付请求 if flow.request.url.endswith(/api/v1/payment/create): # 解析请求参数 try: req_json json.loads(flow.request.content.decode(utf-8)) amount req_json.get(amount, 0) # 根据金额模拟不同响应 if amount 10000: # 大额支付返回风控拦截 flow.response http.Response.make( 200, json.dumps({code: 403, msg: 风控拦截请联系客服}), {content-type: application/json} ) elif amount 1: # 无效金额返回参数错误 flow.response http.Response.make( 400, json.dumps({code: 400, msg: 金额不能小于1}), {content-type: application/json} ) else: # 正常返回 flow.response http.Response.make( 200, json.dumps({ code: 0, data: { order_id: fORD{int(time.time())}, pay_url: https://mock-pay.example.com/pay?tokenabc123 } }), {content-type: application/json} ) except Exception as e: flow.response http.Response.make( 400, json.dumps({code: 400, msg: JSON 解析失败}), {content-type: application/json} )启动命令mitmdump -s mock_payment.py --mode regular -p 8080前端只需把 API 域名指向http://localhost:8080就能获得完全可控的响应。这比写 Node.js Express Mock 服务快 10 倍且无需启动额外进程。6.2 集成性能压测用 mitmdump 重放真实流量JMeter 或 k6 的脚本要手写而 mitmproxy 可以直接重放抓包记录# 先抓取一组真实请求保存为 mitmproxy 格式 mitmdump -w traffic.mitm --mode regular -p 8080 # 用 mitmdump 重放并发 10 个线程循环 5 次 mitmdump -n -c traffic.mitm --mode regular \ --set replay_kill_extratrue \ --set replay_ignore_content_typetrue \ --set stream_large_bodies1000000 \ --set console_eventlog_verbositywarning提示-n参数表示不启动交互界面纯后台运行replay_kill_extratrue会丢弃原始请求中不存在的 header如cookie过期避免重放失败replay_ignore_content_typetrue忽略 content-type 不匹配如原始是application/json重放时可能变成text/plain。这是压测真实性的关键开关。6.3 安全边界提醒mitmproxy 的合规使用红线最后必须强调mitmproxy 是强大的工具但使用有明确边界。我在某金融客户现场部署时法务团队给出了三条铁律仅限自有设备严禁在未经许可的设备上安装 mitmproxy 证书。macOS 的「系统」钥匙串一旦被植入所有 App 的 HTTPS 流量都可被解密这违反 PCI DSS 第 4.1 条禁止生产环境mitmproxy 的--mode transparent会修改系统网络栈生产服务器禁用。我们只在开发机和 CI 测试机上运行日志脱敏mitmdump -w保存的.mitm文件包含完整请求体含用户 token、密码明文。必须用--set save_stream_file_filterurl~api/.*过滤敏感路径并启用--set save_stream_file_obfuscatetrue自动脱敏 cookie 和 auth header。这些不是技术限制而是法律底线。工具的价值在于提升效率而非突破边界。我在实际项目中所有 mitmproxy 脚本都经过静态扫描用 Bandit 检查eval/exec调用所有日志文件加密存储AES-256这才是专业开发者的做法。我在实际使用中发现最省时间的配置是把mitmdump -s sku_extractor.py --mode regular -p 8080做成 Alfred Workflow输入关键词「sku」就自动启动再把证书导入流程写成一键 shell 脚本每次系统更新后 30 秒搞定。工具的价值不在于多炫酷而在于它是否真正融入你的工作流成为呼吸般自然的存在。