TLS + Web API 安全 · 03 · TLS 实战:证书、漏洞与抓包中间人

本文为教学用途,所有复现均在作者自建靶场内完成。请勿在未授权的情况下对真实系统进行测试。根据《网络安全法》,未经授权测试、攻击他人系统属于违法犯罪行为。禁止复制本文Payload 对外部站点进行测试,违规使用造成的一切后果由使用者自行负责


一、启动实验靶场

靶场已经写好,一键启动:

复制代码
bash /opt/tls-api-labs/up.sh

它依次做了六件事(每一步的脚本都在 ca/ 和 app/ 下,可以打开看):

  1. 生成自签根 CA (ca/gen-ca.sh)

  2. 用根 CA 签发服务器证书 ,SAN 包含 api.lab / localhost / 127.0.0.1 / 192.168.143.156(ca/gen-server-cert.sh)

  3. 生成一张不受信任的 rogue 证书 ,供中间人演示(ca/gen-rogue-cert.sh)

  4. 生成客户端证书 ,供双向 TLS 演示(ca/gen-client-cert.sh)

  5. 生成 JWT 用的 RSA 密钥对(app/gen-jwt-keys.sh)

  6. 启动后端 API(app/vuln_api.py,监听 127.0.0.1:9443),并把 nginx 配置铺上去、重载

架构是这样的:

复制代码
物理机浏览器 / curl
        │
        ├── http://api.lab:8081  ──┐(明文)
        │                          │
        └── https://api.lab:443  ──┤ nginx ──> 127.0.0.1:9443 (vuln_api.py,明文)
                                   │
        中间人演示 https://127.0.0.1:8443(mitm_demo.py)也转发到这里

关键点:TLS 在 nginx 这里"终止",nginx 到后端是明文。这是现实中最常见的部署方式。

up.sh 用 systemd-run 起后端服务,名字叫 tls-api。查看状态/日志:

复制代码
systemctl status tls-api
journalctl -u tls-api -n 50 --no-pager

改了后端代码后要 systemctl restart tls-api 才生效。


二、用 openssl 手动走一遍握手

openssl s_client 是一个"手工 TLS 客户端",能让我们把握手过程看个通透。

2.1 看 TLS 1.3 握手摘要

复制代码
cd /opt/tls-api-labs
openssl s_client -connect 127.0.0.1:443 -servername api.lab -CAfile ca/ca.crt -tls1_3 -brief </dev/null

参数回顾(详见第 01 篇):-connect 连哪、-servername 设置 SNI、-CAfile 信任哪个 CA、-tls1_3 强制版本、-brief 精简输出、</dev/null 不进入交互。

重点看 Verification: OK :因为我们在 -CAfile 里信任了自签根 CA,所以验证通过。如果去掉 -CAfile:

复制代码
openssl s_client -connect 127.0.0.1:443 -servername api.lab -brief </dev/null 2>&1 | grep -i verify

会看到(真实回显):

复制代码

意思是"找不到签发者的证书、无法验证第一张证书"------因为客户端不认识我们那个自签 CA。

2.2 强制旧版本:看客户端和服务器谁在拒绝

复制代码
openssl s_client -connect 127.0.0.1:443 -tls1_1 </dev/null 2>&1 | head -5

真实回显:

复制代码

注意:这次失败其实是"客户端"自己拒绝的,不是服务器。 新版 OpenSSL(3.x)在库里默认就把 TLS 1.0/1.1 关掉了,所以客户端根本不愿意发起 1.1 的握手,报 no protocols available。

要真正验证"服务器是否接受 1.1",得先把客户端的最低协议降下来,再发一次:

复制代码
cat > /tmp/ossl_old.cnf <<'EOF'
openssl_conf = openssl_init
[openssl_init]
ssl_conf = ssl_sect
[ssl_sect]
system_default = system_default_sect
[system_default_sect]
MinProtocol = None
CipherString = DEFAULT@SECLEVEL=0
EOF
OPENSSL_CONF=/tmp/ossl_old.cnf openssl s_client -connect 127.0.0.1:443 -tls1_1 </dev/null 2>&1 | head -5

真实回显:

复制代码

这次客户端确实把 TLS 1.1 的 ClientHello 发出去了 ,服务器回了一个 protocol version 告警(SSL alert 70) ,明确拒绝。这才证明是 nginx 的 ssl_protocols 配置在服务端拦住了旧版本。

  • MinProtocol = None:让客户端不再自我限制最低版本。

  • CipherString = DEFAULT@SECLEVEL=0:把安全等级降到 0,否则 1.1 可能因为"算法太弱"而被客户端拒绝。

  • OPENSSL_CONF=...:临时用这份配置启动 openssl,不修改系统配置。

所以旧版本是"两边都在拒绝" :客户端默认不用,服务器也明确拒绝------这就是纵深防御 。禁用 TLS 1.0/1.1 是现代安全基线,因为它们是几十年前的设计,存在 BEAST、POODLE 等已知漏洞(见第七节)。

为什么回显里还有 CONNECTED? 那是 TCP 层连上了(TCP 三次握手成功),但 TLS 层协商失败。no peer certificate available 说明 TLS 握手没完成、没拿到证书。

2.3 看完整证书链

复制代码
openssl s_client -connect 127.0.0.1:443 -servername api.lab -CAfile ca/ca.crt -showcerts </dev/null 2>/dev/null | sed -n "/Certificate chain/,/Server certificate/p"

真实回显(节选):

复制代码
Certificate chain
 0 s:C=CN, O=TLS-API Lab, CN=api.lab
   i:C=CN, O=TLS-API Lab, CN=TLS-API Lab Root CA
   a:PKEY: RSA, 2048 (bit); sigalg: sha256WithRSAEncryption
   v:NotBefore: Sep 22 15:31:41 2026 GMT; NotAfter: Dec 25 15:31:41 2028 GMT

这就是第 02 篇讲的信任链。


三、协议版本与加密套件

3.1 版本

版本 状态 说明
SSL 2.0 / 3.0 已废弃 有严重漏洞(DROWN、POODLE),任何现代配置都应禁用
TLS 1.0 / 1.1 已废弃 2020 年前后各大浏览器陆续停止支持;有 BEAST 等漏洞
TLS 1.2 仍在使用 支持 AEAD 套件、前向保密,配置正确时是安全的
TLS 1.3 推荐 1-RTT、强制前向保密、加密更多握手内容

3.2 加密套件(Cipher Suite)

套件名字看着长,其实有固定结构。以 ECDHE-RSA-AES256-GCM-SHA384 为例:

复制代码
ECDHE      密钥交换算法(临时椭圆曲线,提供前向保密)
 RSA       身份认证算法(用 RSA 证书签名)
 AES256    对称加密算法(256 位 AES)
 GCM       对称加密的工作模式(AEAD,自带完整性)
 SHA384    完整性校验用的哈希

TLS 1.3 把密钥交换和认证分开协商,套件名简化成 TLS_AES_256_GCM_SHA384 这种,只剩"对称加密 + 哈希"。

安全要点:

  • 优先选带 ECDHE 的套件(有前向保密)。

  • 对称加密选 AES-GCM 或 ChaCha20-Poly1305(都是 AEAD,加密和完整性一体,比 CBC 模式安全)。

  • 避开 CBC 模式(POODLE、Lucky13)、RC4(已破)、3DES(太弱)。

  • 避开 EXPORT、NULL、anon(匿名,不认证)套件。


四、curl 的证书校验行为

curl 是最常用的命令行 HTTP 工具,它默认严格校验证书。

4.1 不信任 CA → 失败

复制代码
curl -s https://127.0.0.1/api/health ; echo "exit=$?"

退出码 60 = 证书验证失败(自签、过期、域名不符都会是它)。

4.2 信任 CA → 成功

复制代码
curl -s --cacert ca/ca.crt https://127.0.0.1/api/health ; echo " exit=$?"
  • --cacert ca/ca.crt:指定用这个文件作为信任的 CA(相当于把我们的自签 CA 加进信任列表)。

4.3 -k 跳过校验 → 成功但危险

复制代码
curl -sk https://127.0.0.1/api/health ; echo " exit=$?"
  • -k / --insecure:跳过证书验证 。连上了,但"对面是谁"完全没保证------这正是中间人攻击的温床。生产脚本里出现 -k 是危险信号。

4.4 主机名不匹配 → 失败

复制代码
curl -s --cacert ca/ca.crt --resolve api2.lab:443:127.0.0.1 https://api2.lab/api/health ; echo "exit=$?"

证书 SAN 里没有 api2.lab,主机名校验失败。

4.5 用 curl 看握手细节

复制代码
curl -sv --cacert ca/ca.crt https://127.0.0.1/api/health 2>&1 | grep -E "SSL connection|subject:|issuer:|TLSv|ALPN|HTTP/"

真实回显:

复制代码

这就是第 01 篇画的握手图在真实工具里的样子。-v 是 verbose(详细模式),2>&1 把错误输出(curl 的调试信息走 stderr)合并到标准输出才能被 grep 到。


五、抓包对比:明文 vs 加密

这是理解 TLS 价值最直观的实验。

5.1 抓 HTTP 明文(8081)

复制代码
cd /opt/tls-api-labs
(timeout 6 tcpdump -i lo -A -s0 "port 8081" > /tmp/plain.txt 2>/dev/null &)
sleep 1
curl -s http://127.0.0.1:8081/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"password1"}' >/dev/null
sleep 6
grep -aE "POST|username|password|Host:" /tmp/plain.txt | head

真实回显:

复制代码

账号密码明文可见。

5.2 抓 HTTPS 流量(443)

复制代码
(timeout 6 tcpdump -i lo -s0 -w /tmp/https.pcap "port 443" >/dev/null 2>&1 &)
sleep 1
curl -s -k https://127.0.0.1/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"password1"}' >/dev/null
sleep 5
​
echo "搜索明文 password 的次数:"
tcpdump -r /tmp/https.pcap -A 2>/dev/null | grep -ac "password" || echo "0"
echo "抓到的包(前几行):"
tcpdump -r /tmp/https.pcap 2>/dev/null | head -6

真实回显:

复制代码

搜索 password 次数为 0 ------加密之后,抓包工具只能看到 TCP 层的信息(源端口、标志位、长度),看不到内容。-w /tmp/https.pcap 是把原始包写成 pcap 文件(可以用 Wireshark 打开),-r 是读取 pcap 文件回放。

既然抓包看不到内容,攻击者是不是就没办法了?

不一定。攻击者还有两条路:一是拿到服务器私钥(没有前向保密就能解历史流量),二是中间人------在握手阶段插进去。


六、中间人攻击(MITM)演示

6.1 原理

中间人攻击的核心是:攻击者插在客户端和服务器中间,对客户端冒充服务器,对服务器冒充客户端。

复制代码
客户端 ──TLS──> 攻击者 ──TLS──> 真服务器
        (看到明文)   (重新加密)

攻击者要成功,必须让客户端相信"攻击者的证书就是真服务器的证书"。如果他拿不到受信任的证书,只能用自签证书。这时候:

  • 客户端正确校验证书 → 发现不受信任 → 拒绝连接,攻击失败。

  • 客户端不校验证书 (点了"继续访问" / 用了 -k / App 里写死了忽略证书校验)→ 连接成功,攻击者拿到明文。

6.2 动手:起一个中间人

我们的 app/mitm_demo.py 用那张 rogue 证书在 127.0.0.1:8443 提供 TLS,解密后再转发给真后端,并把明文打印出来。

复制代码
cd /opt/tls-api-labs
(timeout 12 python3 app/mitm_demo.py > /tmp/mitm.log 2>&1 &)
sleep 1.5
​
echo "### 客户端【校验证书】访问 MITM 端口:"
curl -s --cacert ca/ca.crt https://127.0.0.1:8443/api/health 2>&1 ; echo "exit=$?"
​
echo "### 客户端【不校验证书】访问 MITM 端口:"
curl -s -k -X POST https://127.0.0.1:8443/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"password1"}' ; echo
​
sleep 1
echo "### 中间人截获到的明文:"
head -20 /tmp/mitm.log

真实回显:

复制代码

结论:

  • 校验证书时,中间人直接被拒(exit=60)。

  • 不校验时,中间人拿到了完整明文,包括账号密码。

这就是"为什么不能忽略证书警告"的最好证明。

6.3 防御中间人

防御 说明
正确校验证书 客户端/App 不要关掉校验,不要盲目 -k、不要"信任所有证书"
用受信任 CA 签发的证书 用户不会被警告训练成"点继续"
HSTS 强制浏览器以后只用 HTTPS,防止降级到 HTTP 再被劫持
证书固定(Certificate Pinning) App 里写死只信任某张证书/某个公钥,即使系统信任了假 CA 也没用
关注证书告警 任何证书异常都应停下排查,而不是"继续访问"

七、TLS 常见漏洞史

漏洞 年份 影响 原理一句话
Heartbleed 2014 OpenSSL 1.0.1 心跳扩展的越界读,攻击者能读到服务器内存(可能含私钥),一个请求就能泄露数据
POODLE 2014 SSL 3.0 CBC 填充 oracle,把 SSL 3.0 降级后逐字节解密
BEAST 2011 TLS 1.0 CBC IV 可预测,逐字节解密
DROWN 2016 SSLv2 用支持 SSLv2 的服务器去解密 TLS 流量
降级攻击 ------ 版本协商 攻击者篡改握手,逼双方用最弱的版本/算法

防御主线:禁用 SSLv2/3 和 TLS 1.0/1.1;只用 TLS 1.2/1.3 + 强套件;开启 HSTS;及时升级 OpenSSL;正确校验证书。

本实验的 OpenSSL 3.5.5 早已修复 Heartbleed,默认也禁用了旧协议。


八、物理机浏览器实操:导入自签 CA

前面都是用 curl 在命令行里看证书。这一节我们把自签 CA 装进物理机浏览器,用眼睛看"证书告警 → 受信任"的变化。

前提:物理机与远端同网段,且已在 hosts 里加了 192.168.143.156 api.lab

8.1 先把 CA 下载到物理机

在物理机上执行:

复制代码
sshpass -p '123' scp root@192.168.143.156:/opt/tls-api-labs/ca/ca.crt ./ca.crt

scp 是"远程拷贝",把远端文件下到本地;后面两个参数是 远端路径 和 本地目标。注意 ca.key(私钥)绝对不能 下载或外传,只需要 ca.crt(证书)。

8.2 导入前的样子(反面教材)

浏览器打开 https://api.lab。因为浏览器不认识这个自签 CA,会拦截:

  • Chrome/Edge:您的连接不是私密连接 / NET::ERR_CERT_AUTHORITY_INVALID,需要点"高级 → 继续前往"才能进。

  • 点"继续前往"后地址栏显示**"不安全"**(红色/带感叹号),不是锁。

攻击者用自签证书做中间人时,用户看到的就是它。点"继续"等于放弃身份认证。

8.3 导入 CA 到"受信任的根证书颁发机构"

导入的原理:把我们的 ca.crt 加进客户端的信任锚列表,之后它签发的证书就能一路验通。

  • Windows :双击 ca.crt → "安装证书" → 存储位置选"本地计算机" → "将所有的证书都放入下列存储" → 受信任的根证书颁发机构 → 完成。之后可能需要重启浏览器。

  • macOS :双击 ca.crt 导入"钥匙串访问" → 找到该证书 → 右键"显示简介" → "信任" → 把"使用此证书时"设为"始终信任"。

  • Linux(系统级,Debian/Ubuntu):

    复制代码
    sudo cp ca.crt /usr/local/share/ca-certificates/tls-api-lab.crt
    sudo update-ca-certificates

    第一条把证书放进系统目录,第二条重建系统信任库。

  • Firefox (它用自己独立的信任库,不跟随系统):设置 → 隐私与安全 → 证书 → 查看证书 → "证书颁发机构" → 导入 → 勾选"信任由此证书颁发机构来标识网站"。

为什么 Firefox 要单独导入?

因为 Firefox 不读操作系统的信任库,而是维护自己的 NSS 证书库。Chrome/Edge 在 Windows/macOS 上跟随系统,在 Linux 上则可能用 NSS。所以"导入后 Chrome 好了、Firefox 还报警"是正常现象。

8.4 导入后的样子(正常网站)

再打开 https://api.lab:地址栏出现锁形图标,不再有警告。点锁 → "连接是安全的" → "证书有效" → 可以查看:

  • 证书链:TLS-API Lab Root CA → api.lab。

  • 有效期、SAN(api.lab、localhost、127.0.0.1、192.168.143.156)。

  • 公钥算法、签名算法。

8.5 用 DevTools 看 API 请求

按 F12(或右键 → 检查)打开开发者工具,切到 Network(网络) 面板,刷新页面或访问 https://api.lab/api/health:

  • Headers :能看到请求方法、URL、状态码,以及 Authorization: Bearer ... 这类请求头。

  • Response:能看到返回的 JSON。

  • Application/存储 :很多前端会把 token 放在 localStorage,在这里能直接看到(这也是它容易被 XSS 偷走的原因)。

8.6 实验做完,把 CA 删掉

自签 CA 只应存在于实验环境。做完实验建议移除:

  • Windows:certmgr.msc → 受信任的根证书颁发机构 → 找到并删除。

  • macOS:钥匙串访问里删除。

  • Linux:sudo rm /usr/local/share/ca-certificates/tls-api-lab.crt && sudo update-ca-certificates --fresh。


九、现实世界:生产环境怎么配 TLS

配置项 生产建议 为什么
协议版本 只开 TLS 1.2 / 1.3 旧版本有已知漏洞
套件 只留 AEAD + ECDHE 前向保密 + 完整性
证书 Let's Encrypt / 商业 CA,自动续期 避免过期事故
HSTS 开启,max-age 至少半年 防降级、防 Cookie 劫持
OCSP Stapling 开启 提速、保护隐私
私钥权限 600,仅服务进程可读 私钥泄露等于身份被冒用
证书监控 到期前告警 过期 = 全站挂
重定向 80 端口 301 到 443 避免用户走明文

关于 HSTS 的坑 :HSTS 一旦被浏览器记住,在 max-age 内就算证书出问题,用户也没法点"继续访问"------这是好事(安全),但调试自签证书时很麻烦。所以本实验默认没开 HSTS,避免把你的浏览器"锁死"。生产环境则应该开。


十、本篇小结

  1. 靶场 up.sh 一键起:自签 CA + 服务器证书 + nginx(443) + 后端 API(9443) + 明文对比(8081)。

  2. openssl s_client 能手动走握手、看版本/套件/证书链;默认情况下 OpenSSL 自己就拒绝 TLS1.1(客户端限制),把客户端最低协议降下来后,服务器会回 protocol version 告警(服务端限制)。

  3. curl 默认严格校验:不信任 CA → 退出码 60;-k 能连但危险;主机名不符也报 60。

  4. tcpdump 抓包:HTTP 明文可见,HTTPS 搜不到明文。

  5. 中间人:客户端校验 → 被拒;不校验 → 明文全泄露。

  6. 防御主线:禁用旧版本、强套件、正确校验证书、HSTS、证书固定。


十一、总结

  1. openssl s_client 的 -servername 是干什么的?不加会怎样?

    答 :它设置 SNI,告诉服务器"我要访问哪个域名",服务器据此决定出示哪张证书。不加的话,服务器可能返回默认站点的证书(域名对不上),导致校验失败,或者把你导向错误的网站。

  2. curl 退出码 60 代表什么?有哪几种原因会导致它?

    答 :代表证书验证失败。原因包括:自签/不受信任的 CA、证书过期、访问域名与证书 SAN 不符、缺少中间证书、客户端系统时间不对、证书被吊销等。

  3. 为什么抓 HTTPS 的包看不到内容?中间人又是怎么看到明文的?

    答 :HTTPS 的应用数据被对称加密 ,抓包只能看到 TCP 头和加密的 TLS 记录,看不到明文。中间人不是在"解密",而是在握手阶段插进中间 :对客户端用一张(不受信任的)证书冒充服务器,客户端若不校验就会和中间人建立加密连接、把明文交给它,中间人再重新加密转发给真服务器。

  4. 中间人攻击成功的前提是什么?怎么防?

    答 :前提是客户端没有正确校验证书 (点了"继续访问"、用了 -k、App 里写死忽略校验),或者客户端系统被安装了攻击者的根 CA。防御:正确校验证书、用受信任 CA 签发的证书、开启 HSTS、App 做证书固定(pinning)。

  5. ssl_protocols TLSv1.2 TLSv1.3; 这行配置为什么重要?

    答 :它让 nginx 只接受 TLS 1.2/1.3,在服务端拦住了存在已知漏洞(BEAST、POODLE、DROWN 等)的 SSL 3.0 和 TLS 1.0/1.1,缩小了攻击面。允许旧版本等于给攻击者留了"降级"的口子。

  6. HSTS 有什么用?为什么调试自签证书时不建议开?

    答 :HSTS 让浏览器在有效期内强制只用 HTTPS 访问该站,防止降级到 HTTP 被劫持。调试自签证书时不建议开,是因为 HSTS 一旦被浏览器记住,在 max-age 内就算证书有问题也不允许"继续访问",会把你锁在打不开的状态,很麻烦。

相关推荐
大棉花哥哥5 小时前
太空安全,正式上线 —— 博客改版与双专栏发布记
安全·网络安全·卫星安全
Lsetea5 小时前
OpenSSL s_client退出码为0却证书验证失败:严格校验与Shell管道排查
https·shell·ssl证书·openssl·tls
光依旧8 小时前
MCP实战手记(九):生产化MCP Server的6层安全防护
java·人工智能·spring boot·安全·网络安全·ai agent·mcp
admin and root16 小时前
「AI安全篇」实战AntiDebug自动化JS逆向加解密MCP
javascript·人工智能·网络安全·自动化·漏洞挖掘·cnvd·src赏金
泛联新安19 小时前
软件定义汽车时代,如何让AI研发“可信”?——泛联新安构建汽车企业级可信AI体系的落地路径
大数据·人工智能·安全·网络安全·汽车·漏洞挖掘·代码安全
菩提小狗1 天前
每日安全情报报告 · 2026-09-29
网络安全·漏洞·cve·安全情报·每日安全
Daorigin_com1 天前
道本科技携手DeepSeek:以AI重塑合同全生命周期管理
前端·人工智能·科技·网络安全·数据挖掘·前端框架·传媒
白帽攻防录1 天前
SRC 挖洞:Apache Tomcat 加密拦截器绕过深度复盘,CVE-2026-34486 fail-open 一行代码怎么打穿集群通信
java·网络安全·tomcat·apache
学逆向的1 天前
win32消息类型
windows·网络安全·mfc·api·win32