本文为教学用途,所有复现均在作者自建靶场内完成。请勿在未授权的情况下对真实系统进行测试。根据《网络安全法》,未经授权测试、攻击他人系统属于违法犯罪行为。禁止复制本文Payload 对外部站点进行测试,违规使用造成的一切后果由使用者自行负责
一、启动实验靶场
靶场已经写好,一键启动:
bash /opt/tls-api-labs/up.sh
它依次做了六件事(每一步的脚本都在 ca/ 和 app/ 下,可以打开看):
-
生成自签根 CA (
ca/gen-ca.sh) -
用根 CA 签发服务器证书 ,SAN 包含
api.lab / localhost / 127.0.0.1 / 192.168.143.156(ca/gen-server-cert.sh) -
生成一张不受信任的 rogue 证书 ,供中间人演示(
ca/gen-rogue-cert.sh) -
生成客户端证书 ,供双向 TLS 演示(
ca/gen-client-cert.sh) -
生成 JWT 用的 RSA 密钥对(
app/gen-jwt-keys.sh) -
启动后端 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,避免把你的浏览器"锁死"。生产环境则应该开。
十、本篇小结
-
靶场
up.sh一键起:自签 CA + 服务器证书 + nginx(443) + 后端 API(9443) + 明文对比(8081)。 -
openssl s_client能手动走握手、看版本/套件/证书链;默认情况下 OpenSSL 自己就拒绝 TLS1.1(客户端限制),把客户端最低协议降下来后,服务器会回protocol version告警(服务端限制)。 -
curl 默认严格校验:不信任 CA → 退出码 60;
-k能连但危险;主机名不符也报 60。 -
tcpdump 抓包:HTTP 明文可见,HTTPS 搜不到明文。
-
中间人:客户端校验 → 被拒;不校验 → 明文全泄露。
-
防御主线:禁用旧版本、强套件、正确校验证书、HSTS、证书固定。
十一、总结
-
openssl s_client的-servername是干什么的?不加会怎样?答 :它设置 SNI,告诉服务器"我要访问哪个域名",服务器据此决定出示哪张证书。不加的话,服务器可能返回默认站点的证书(域名对不上),导致校验失败,或者把你导向错误的网站。
-
curl 退出码 60 代表什么?有哪几种原因会导致它?
答 :代表证书验证失败。原因包括:自签/不受信任的 CA、证书过期、访问域名与证书 SAN 不符、缺少中间证书、客户端系统时间不对、证书被吊销等。
-
为什么抓 HTTPS 的包看不到内容?中间人又是怎么看到明文的?
答 :HTTPS 的应用数据被对称加密 ,抓包只能看到 TCP 头和加密的 TLS 记录,看不到明文。中间人不是在"解密",而是在握手阶段插进中间 :对客户端用一张(不受信任的)证书冒充服务器,客户端若不校验就会和中间人建立加密连接、把明文交给它,中间人再重新加密转发给真服务器。
-
中间人攻击成功的前提是什么?怎么防?
答 :前提是客户端没有正确校验证书 (点了"继续访问"、用了
-k、App 里写死忽略校验),或者客户端系统被安装了攻击者的根 CA。防御:正确校验证书、用受信任 CA 签发的证书、开启 HSTS、App 做证书固定(pinning)。 -
ssl_protocols TLSv1.2 TLSv1.3;这行配置为什么重要?答 :它让 nginx 只接受 TLS 1.2/1.3,在服务端拦住了存在已知漏洞(BEAST、POODLE、DROWN 等)的 SSL 3.0 和 TLS 1.0/1.1,缩小了攻击面。允许旧版本等于给攻击者留了"降级"的口子。
-
HSTS 有什么用?为什么调试自签证书时不建议开?
答 :HSTS 让浏览器在有效期内强制只用 HTTPS 访问该站,防止降级到 HTTP 被劫持。调试自签证书时不建议开,是因为 HSTS 一旦被浏览器记住,在
max-age内就算证书有问题也不允许"继续访问",会把你锁在打不开的状态,很麻烦。