中间人攻击 (MITM) 原理与防护
🎯 理解 HTTPS 被抓包的原理,以及如何防护敏感数据
1. 什么是中间人攻击
1.1 基本概念
┌─────────────────────────────────────────────────────────────────────────┐
│ 中间人攻击 (Man-in-the-Middle) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 定义: │
│ 攻击者秘密地拦截并可能修改双方之间的通信,双方都认为自己在直接通信 │
│ │
│ 正常通信: │
│ ┌────────┐ ┌────────┐ │
│ │ Client │ ◀══════════ 加密通信 ══════════▶ │ Server │ │
│ └────────┘ └────────┘ │
│ │
│ 中间人攻击: │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Client │ ◀───▶ │ Attacker│ ◀───▶ │ Server │ │
│ └────────┘ │ (MITM) │ └────────┘ │
│ │ └────────┘ │ │
│ │ │ │ │
│ │ ① 截获请求 ③ 转发请求 │
│ │ ② 读取/修改数据 ④ 截获响应 │
│ │ ⑤ 修改/转发给客户端 │
│ │
│ 危害: │
│ • 窃取敏感信息 (密码、Token、信用卡) │
│ • 篡改数据 (注入恶意代码) │
│ • 会话劫持 │
│ • 钓鱼攻击 │
│ │
└─────────────────────────────────────────────────────────────────────────┘1.2 常见的 MITM 攻击方式
┌─────────────────────────────────────────────────────────────────────────┐
│ 常见 MITM 攻击方式 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. ARP 欺骗 (局域网) │
│ ───────────────────── │
│ 攻击者发送伪造的 ARP 响应,将网关 IP 绑定到自己的 MAC 地址 │
│ │
│ 正常: 网关 192.168.1.1 → MAC_Gateway │
│ 攻击: 网关 192.168.1.1 → MAC_Attacker (伪造) │
│ │
│ 受害者的所有流量都会经过攻击者 │
│ │
│ 2. DNS 劫持 │
│ ───────────── │
│ 篡改 DNS 响应,将域名解析到攻击者服务器 │
│ │
│ 正常: bank.com → 1.2.3.4 (真实服务器) │
│ 攻击: bank.com → 5.6.7.8 (钓鱼服务器) │
│ │
│ 3. Wi-Fi 钓鱼 (Evil Twin) │
│ ───────────────────────── │
│ 创建同名的假 Wi-Fi 热点,诱导用户连接 │
│ │
│ 真实: Starbucks_WiFi (官方) │
│ 钓鱼: Starbucks_WiFi (攻击者控制,信号更强) │
│ │
│ 4. SSL 剥离 (SSL Stripping) │
│ ─────────────────────────── │
│ 强制将 HTTPS 降级为 HTTP │
│ │
│ 用户 ──HTTP──▶ 攻击者 ──HTTPS──▶ 服务器 │
│ │
│ 用户看到的是 HTTP,攻击者可以读取明文 │
│ │
└─────────────────────────────────────────────────────────────────────────┘2. HTTPS 为什么能防止 MITM
2.1 HTTPS 握手流程
┌─────────────────────────────────────────────────────────────────────────┐
│ TLS 1.2 握手流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ │ │ │
│ │─────── ClientHello ─────────────────────────▶│ │
│ │ (支持的加密套件、随机数 A) │ │
│ │ │ │
│ │◀────── ServerHello ──────────────────────────│ │
│ │ (选定的加密套件、随机数 B) │ │
│ │ │ │
│ │◀────── Certificate ──────────────────────────│ │
│ │ (服务器证书) │ │
│ │ │ │
│ │◀────── ServerHelloDone ─────────────────────│ │
│ │ │ │
│ │ [客户端验证证书] │ │
│ │ 1. 检查证书是否由受信任 CA 签发 │ │
│ │ 2. 检查证书是否过期 │ │
│ │ 3. 检查域名是否匹配 │ │
│ │ │ │
│ │─────── ClientKeyExchange ───────────────────▶│ │
│ │ (用服务器公钥加密的预主密钥) │ │
│ │ │ │
│ │─────── ChangeCipherSpec ────────────────────▶│ │
│ │ │ │
│ │─────── Finished (加密) ─────────────────────▶│ │
│ │ │ │
│ │◀────── ChangeCipherSpec ─────────────────────│ │
│ │ │ │
│ │◀────── Finished (加密) ──────────────────────│ │
│ │ │ │
│ │◀═══════ 加密通信 ═══════════════════════════▶│ │
│ │
│ 会话密钥 = PRF(预主密钥, 随机数A, 随机数B) │
│ │
└─────────────────────────────────────────────────────────────────────────┘2.2 证书链验证
┌─────────────────────────────────────────────────────────────────────────┐
│ 证书链验证 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 证书链结构: │
│ ─────────── │
│ │
│ ┌─────────────────────┐ │
│ │ Root CA 证书 │ ← 预装在操作系统/浏览器中 │
│ │ (自签名) │ ← 信任锚点 │
│ └──────────┬──────────┘ │
│ │ 签发 │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 中间 CA 证书 │ ← 由 Root CA 签发 │
│ │ │ │
│ └──────────┬──────────┘ │
│ │ 签发 │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 服务器证书 │ ← 由中间 CA 签发 │
│ │ (example.com) │ ← 包含域名和公钥 │
│ └─────────────────────┘ │
│ │
│ 验证过程: │
│ ───────── │
│ 1. 服务器发送: 服务器证书 + 中间 CA 证书 │
│ 2. 浏览器用中间 CA 公钥验证服务器证书签名 │
│ 3. 浏览器用 Root CA 公钥验证中间 CA 证书签名 │
│ 4. Root CA 在信任列表中 → 验证通过 │
│ │
│ 如果任何一步验证失败: │
│ • 签名不匹配 │
│ • 证书过期 │
│ • 域名不匹配 │
│ • CA 不受信任 │
│ → 浏览器显示安全警告 │
│ │
└─────────────────────────────────────────────────────────────────────────┘3. Charles/Fiddler 如何抓取 HTTPS
3.1 抓包工具的 MITM 原理
┌─────────────────────────────────────────────────────────────────────────┐
│ Charles 抓取 HTTPS 的原理 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 前提条件: │
│ ───────── │
│ 用户必须手动安装并信任 Charles 的根证书 │
│ │
│ 步骤详解: │
│ ───────── │
│ │
│ ┌────────┐ ┌─────────┐ ┌────────┐ │
│ │ Browser│ │ Charles │ │ Server │ │
│ └────┬───┘ └────┬────┘ └───┬────┘ │
│ │ │ │ │
│ │ ① ClientHello │ │ │
│ │────────────────▶│ │ │
│ │ │ ② ClientHello │ │
│ │ │─────────────────▶│ │
│ │ │ │ │
│ │ │ ③ 真实证书 │ │
│ │ │◀─────────────────│ │
│ │ │ │ │
│ │ ④ 伪造证书 │ │ │
│ │◀────────────────│ (Charles 动态 │ │
│ │ (域名相同, │ 生成,用自己 │ │
│ │ Charles CA │ 的 CA 签发) │ │
│ │ 签发) │ │ │
│ │ │ │ │
│ │ ⑤ 验证证书 ✅ │ │ │
│ │ (Charles CA │ │ │
│ │ 已被信任) │ │ │
│ │ │ │ │
│ │ ⑥ 发送数据 │ │ │
│ │════════════════▶│ ⑦ 解密 + 转发 │ │
│ │ (TLS 1) │════════════════▶│ │
│ │ │ (TLS 2) │ │
│ │ │ │ │
│ │ ⑨ 返回数据 │ ⑧ 响应数据 │ │
│ │◀════════════════│◀════════════════│ │
│ │ (重新加密) │ (解密查看) │ │
│ │
│ 关键点: │
│ • Charles 建立两条独立的 TLS 连接 │
│ • 浏览器 ◀─TLS─▶ Charles ◀─TLS─▶ 服务器 │
│ • Charles 能看到明文数据 │
│ • 如果用户未信任 Charles CA,浏览器会报错 │
│ │
└─────────────────────────────────────────────────────────────────────────┘3.2 Charles 证书安装流程
bash
# macOS 安装 Charles 根证书
# 1. Charles → Help → SSL Proxying → Install Charles Root Certificate
# 2. 钥匙串访问 → 系统 → 找到 Charles Proxy CA
# 3. 双击 → 信任 → 始终信任
# iOS 安装
# 1. 配置代理指向 Charles
# 2. Safari 访问 chls.pro/ssl
# 3. 设置 → 通用 → VPN与设备管理 → 安装证书
# 4. 设置 → 通用 → 关于本机 → 证书信任设置 → 启用完全信任
# Android 安装 (需要 root 或 7.0 以下)
# Android 7.0+ 默认不信任用户安装的 CA 证书
# 需要: 1) root 安装到系统 CA 2) APP 配置 network-security-config3.3 为什么 Charles 能抓包,黑客不能?
┌─────────────────────────────────────────────────────────────────────────┐
│ Charles vs 真实攻击者 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Charles (合法抓包): │
│ ─────────────────── │
│ ✅ 用户主动安装并信任 Charles 根证书 │
│ ✅ Charles 用自己的 CA 签发伪证书 │
│ ✅ 浏览器信任该 CA → 验证通过 │
│ ✅ 用户知道自己在做什么 │
│ │
│ 攻击者 (非法 MITM): │
│ ───────────────────── │
│ ❌ 攻击者的 CA 不在系统信任列表中 │
│ ❌ 伪造的证书无法通过验证 │
│ ❌ 浏览器显示安全警告: "您的连接不是私密连接" │
│ ❌ 用户如果忽略警告继续访问,才会被攻击 │
│ │
│ 关键区别: │
│ ───────── │
│ 合法的 CA (DigiCert, Let's Encrypt 等) 不会为攻击者签发证书 │
│ 攻击者只能用自签名证书,浏览器不信任 │
│ │
│ 例外情况 (真实威胁): │
│ ───────────────── │
│ 1. 企业内网: IT 部门在员工电脑安装了监控 CA │
│ 2. 恶意软件: 安装了恶意 CA 到用户系统 │
│ 3. CA 被入侵: 历史上有 CA 被入侵签发恶意证书 (DigiNotar) │
│ 4. 国家级攻击: 某些国家强制安装政府 CA │
│ │
└─────────────────────────────────────────────────────────────────────────┘4. 前端如何防护 MITM
4.1 HSTS (HTTP Strict Transport Security)
http
# 服务器响应头
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload┌─────────────────────────────────────────────────────────────────────────┐
│ HSTS 防护原理 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 没有 HSTS (可被 SSL 剥离攻击): │
│ ──────────────────────────── │
│ 用户输入: bank.com │
│ ↓ │
│ 浏览器请求: http://bank.com │
│ ↓ │
│ 攻击者拦截,不转发 HTTPS 重定向 │
│ ↓ │
│ 用户一直使用 HTTP (明文) │
│ │
│ 有 HSTS: │
│ ──────── │
│ 用户输入: bank.com │
│ ↓ │
│ 浏览器检查 HSTS 缓存 │
│ ↓ │
│ 强制使用 https://bank.com (不发 HTTP 请求) │
│ ↓ │
│ 即使攻击者在中间也无法降级 │
│ │
│ HSTS Preload List: │
│ ────────────────── │
│ Chrome/Firefox 内置的 HSTS 站点列表 │
│ 首次访问就强制 HTTPS,无需先访问一次 │
│ 提交: https://hstspreload.org │
│ │
└─────────────────────────────────────────────────────────────────────────┘4.2 证书固定 (Certificate Pinning)
javascript
// 原理: 客户端预置服务器证书的公钥指纹,拒绝其他证书
// 方案 1: Public-Key-Pins (已废弃,风险太高)
// 方案 2: 客户端代码内置校验
// React Native / 移动端示例
const pinnedCertificates = {
'api.example.com': [
'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // 主证书
'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=', // 备份证书
],
};
// 请求时校验 (示例,实际实现依赖平台)
async function secureFetch(url) {
const response = await fetch(url);
// 获取证书指纹 (需要平台支持)
const certFingerprint = await getCertificateFingerprint(url);
const hostname = new URL(url).hostname;
if (!pinnedCertificates[hostname]?.includes(certFingerprint)) {
throw new Error('Certificate pinning failed');
}
return response;
}xml
<!-- Android network-security-config.xml -->
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2025-01-01">
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
</network-security-config>4.3 前端检测 MITM
javascript
// 方法 1: 检测证书是否被替换
async function detectMITM() {
try {
const response = await fetch('/api/cert-check');
const serverCertHash = response.headers.get('X-Cert-Hash');
// 预置的正确证书哈希
const expectedHash = 'sha256:abc123...';
if (serverCertHash !== expectedHash) {
console.error('Possible MITM detected!');
// 上报、告警、阻止敏感操作
}
} catch (e) {
// 证书验证失败
}
}
// 方法 2: 利用 Service Worker 检测
// 如果 HTTPS 降级为 HTTP,Service Worker 无法注册
if ('serviceWorker' in navigator && location.protocol === 'https:') {
navigator.serviceWorker.register('/sw.js')
.then(() => console.log('HTTPS verified'))
.catch(() => console.error('Possible downgrade attack'));
}
// 方法 3: 监控 Security 状态
if (window.isSecureContext === false) {
console.error('Not in secure context!');
}4.4 敏感操作二次加密
javascript
// 即使 HTTPS 被破解,敏感数据仍然加密
import { box, randomBytes } from 'tweetnacl';
import { encodeBase64, decodeBase64 } from 'tweetnacl-util';
// 服务端公钥 (内置在代码中)
const serverPublicKey = decodeBase64('服务器公钥');
function encryptSensitiveData(data) {
// 生成临时密钥对
const clientKeyPair = box.keyPair();
const nonce = randomBytes(box.nonceLength);
// 加密数据
const message = new TextEncoder().encode(JSON.stringify(data));
const encrypted = box(message, nonce, serverPublicKey, clientKeyPair.secretKey);
return {
ciphertext: encodeBase64(encrypted),
nonce: encodeBase64(nonce),
clientPublicKey: encodeBase64(clientKeyPair.publicKey),
};
}
// 登录时加密密码
async function login(username, password) {
const encryptedPassword = encryptSensitiveData({ password });
return fetch('/api/login', {
method: 'POST',
body: JSON.stringify({
username,
...encryptedPassword,
}),
});
}5. 真实案例分析
案例 1: DigiNotar CA 被入侵 (2011)
┌─────────────────────────────────────────────────────────────────────────┐
│ DigiNotar 事件 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 事件经过: │
│ ───────── │
│ 1. 黑客入侵荷兰 CA DigiNotar │
│ 2. 签发了 500+ 伪造证书,包括 *.google.com │
│ 3. 伊朗政府使用这些证书监控公民 Gmail │
│ 4. 约 30 万 IP 受影响 │
│ │
│ 攻击方式: │
│ ───────── │
│ ISP 级别 DNS 劫持 + 伪造的 Google 证书 │
│ 用户访问 Gmail 时,DNS 解析到攻击者服务器 │
│ 攻击者使用 DigiNotar 签发的伪证书,浏览器不报警 │
│ │
│ 后果: │
│ ────── │
│ • DigiNotar 被所有浏览器吊销信任,公司破产 │
│ • Chrome 引入证书透明度 (Certificate Transparency) │
│ • 浏览器加强证书吊销检查 │
│ │
│ 教训: │
│ ────── │
│ • CA 是信任链的关键,一旦失守,HTTPS 形同虚设 │
│ • 证书固定 (Pinning) 可以防御此类攻击 │
│ • Google 后来实施了公钥固定 (HPKP,现已废弃) │
│ │
└─────────────────────────────────────────────────────────────────────────┘案例 2: 企业 HTTPS 监控
┌─────────────────────────────────────────────────────────────────────────┐
│ 企业 HTTPS 监控 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 场景: │
│ ────── │
│ 很多企业会在员工电脑安装企业 CA,监控 HTTPS 流量 │
│ 用于防止数据泄露、检测恶意软件 │
│ │
│ 实现方式: │
│ ───────── │
│ 1. IT 通过组策略在员工电脑安装企业根 CA │
│ 2. 企业防火墙/代理充当 MITM │
│ 3. 员工所有 HTTPS 流量都经过企业代理解密检查 │
│ │
│ 前端开发者注意: │
│ ────────────── │
│ • 在企业网络中测试时,证书可能是企业 CA 签发的 │
│ • 部分 API 可能因证书固定失败 │
│ • 某些安全测试结果可能不准确 │
│ │
│ 检测方法: │
│ ───────── │
│ // 在控制台检查证书颁发者 │
│ // Chrome DevTools → Security → View certificate │
│ // 如果 Issuer 不是知名 CA,可能在被监控 │
│ │
└─────────────────────────────────────────────────────────────────────────┘案例 3: 12306 自签名证书
┌─────────────────────────────────────────────────────────────────────────┐
│ 12306 证书问题 (历史) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 历史情况 (2010s): │
│ ───────────────── │
│ 12306 使用 SRCA (中铁数字证书认证中心) 签发的证书 │
│ SRCA 不在主流浏览器信任列表中 │
│ 用户需要手动下载安装 SRCA 根证书 │
│ │
│ 问题: │
│ ────── │
│ 1. 用户访问时看到 "不安全" 警告 │
│ 2. 手动安装证书的做法培养了忽视安全警告的习惯 │
│ 3. 如果有钓鱼网站,用户可能也会忽视警告 │
│ │
│ 现状 (2020s): │
│ ───────────── │
│ 12306 已切换到 DigiCert 等国际 CA 签发的证书 │
│ 访问时不再有安全警告 │
│ │
│ 教训: │
│ ────── │
│ 不要引导用户安装非标准 CA,这会: │
│ • 降低用户安全意识 │
│ • 增加钓鱼攻击风险 │
│ • 影响用户体验 │
│ │
└─────────────────────────────────────────────────────────────────────────┘6. 开发调试指南
6.1 正确使用 Charles/Fiddler
bash
# 1. 仅在开发环境使用
# 2. 不要在生产设备上安装抓包工具的 CA
# 3. 测试完成后移除信任的 CA
# Charles 配置最佳实践
# Proxy → SSL Proxying Settings
# 仅添加需要抓包的域名,不要用 *
# 示例配置:
# Include:
# - api.example.com:443
# - staging.example.com:443
# 不要添加:
# - *:* (会抓所有 HTTPS,包括银行、邮箱)6.2 调试证书问题
javascript
// 浏览器端检查证书信息
// Chrome DevTools → Security 面板
// Node.js 检查证书
const https = require('https');
const tls = require('tls');
function checkCertificate(hostname, port = 443) {
return new Promise((resolve, reject) => {
const socket = tls.connect(port, hostname, {
servername: hostname,
}, () => {
const cert = socket.getPeerCertificate();
console.log('Subject:', cert.subject);
console.log('Issuer:', cert.issuer);
console.log('Valid From:', cert.valid_from);
console.log('Valid To:', cert.valid_to);
console.log('Fingerprint:', cert.fingerprint256);
socket.end();
resolve(cert);
});
socket.on('error', reject);
});
}
checkCertificate('example.com').then(console.log);bash
# OpenSSL 检查证书
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | \
openssl x509 -noout -text
# 检查证书链
openssl s_client -connect example.com:443 -showcerts7. 面试高频问题
Q1: Charles 为什么能抓取 HTTPS?
- 用户手动安装并信任 Charles 根证书
- Charles 动态为每个域名生成伪造证书,用自己的 CA 签发
- 浏览器信任该 CA,验证通过
- Charles 建立两条独立的 TLS 连接,在中间解密查看
Q2: 普通 MITM 攻击者为什么不能像 Charles 一样?
攻击者的 CA 不在系统信任列表中,伪造的证书无法通过浏览器验证,会显示安全警告。除非:
- 用户忽略警告继续访问
- 攻击者在用户设备安装了恶意 CA
- 合法 CA 被入侵签发了恶意证书
Q3: 如何防止 SSL 剥离攻击?
- 启用 HSTS,强制浏览器使用 HTTPS
- 加入 HSTS Preload List,首次访问就强制 HTTPS
- 使用 HTTPS-only 模式 (现代浏览器支持)
Q4: 证书固定 (Pinning) 的优缺点?
优点:
- 防止被入侵的 CA 签发恶意证书
- 额外的安全层
缺点:
- 证书更新时需要同步更新客户端
- 配置错误可能导致服务不可用
- 维护成本高
Q5: 企业环境下如何检测 HTTPS 是否被监控?
- 检查证书颁发者 (Issuer) 是否为知名 CA
- 使用不同网络 (如手机热点) 对比证书指纹
- 访问 https://www.howsmyssl.com 等检测网站