ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HTTP到HTTPS:TLS握手、证书链与中间人攻击实战解析

HTTP到HTTPS:TLS握手、证书链与中间人攻击实战解析 抓包的时候看到账号密码在HTTP请求里裸奔那一刻我是真的有点头皮发麻。HTTP和HTTPS这一对词几乎所有搞过网络的人都能说上两句但真到了线上故障、抓包调试、证书配置的时候坑一个接一个。这篇东西我不打算给你念教科书而是从一个从业者的视角把HTTP到HTTPS这条路上的核心知识点、加密原理、攻防实战和真实排障过程重新捋一遍带点能直接抄作业的内容。先交代一下这篇博文解决什么问题不管你是后端开发、运维、测试还是做嵌入式、写小程序只要你的程序要跟服务器通信就绕不开HTTP和HTTPS。你不需要重新学一遍密码学但你需要知道TLS握手在干什么、证书链为什么那么重要、中间人攻击是怎么发生的、以及日常遇到的报错到底在说什么。适合三类人看一是刚接触网络协议想系统理解的新人二是配置过HTTPS但一遇到证书问题就慌的进阶者三是做测试和运维、天天要跟抓包工具和代理打交道的老手。1. 先搞清楚HTTP为什么需要升级成HTTPS1.1 先回顾HTTP的裸奔本质HTTP超文本传输协议是互联网数据通信最基础的规矩规定了客户端和服务器之间怎么打招呼、怎么交换数据。它跑在TCP之上采用经典的请求-响应模型客户端发一个请求行比如GET /index.html HTTP/1.1附带一堆请求头Headers服务器收到后返回状态行、响应头和响应体。整个过程看着简单但有一个致命的先天缺陷——所有内容都是明文传输。这里的明文不是开玩笑的。从你家路由器到运营商机房再到目标服务器机房请求数据经过的每一个网络节点理论上都可以被抓包工具完整读出来。我当年在一个老旧系统上做测试用Wireshark随便一抓用户的登录账号、密码、Cookie里的SessionID全在那躺着连加密都不用破解直接就能看到。Cookie和SessionID一旦泄露攻击者可以直接拿来冒充用户身份这个叫会话劫持比偷密码还可怕。HTTP还有一个特点是无状态也就是说服务器默认不记得你上一次请求是谁。为了记住你和你的登录状态就引入了Cookie和Session机制服务器生成一个SessionID存在自己的内存或者Redis里同时把这个ID写入客户端的Cookie后续请求带上这个ID服务器才认出你。这套机制本身没问题但它加剧了明文传输的风险——一旦Cookie在链路上被人截获整个登录态就等于拱手让人了。HTTP的第三个问题是没有校验机制。明文传输的消息,中间人改一个字节接收方完全感知不到。举个例子如果你从某个HTTP页面下载一个软件安装包攻击者在链路中间把安装包换成带木马的同名文件你的电脑毫无警觉地就装上了。这种完整性缺失是HTTP作为应用层协议的一个根本性缺陷后面的HTTPS核心就是补这个洞。1.2 连接复用与性能的演进讲完安全问题说说HTTP在工程上的另一个坑——连接复用。早期的HTTP/1.0版本浏览器每次请求一个资源都要新建一次TCP连接而TCP建连是要三次握手的。一个网页往往有几十个资源图片、CSS、JS文件每个都握手、挥手性能差到离谱。所以HTTP/1.1推出了keep-alive机制默认开启Connection: keep-alive让同一个TCP连接可以被多个请求复用省掉了大量重复握手开销。这就是http连接复用这个热词的出处。但keep-alive也有它的别扭之处它只能串行处理请求前一个响应没回来后一个请求就得排队这就是HTTP/1.1的队头阻塞问题。后来HTTP/2引入多路复用在同一个TCP连接上并发传输多个流Stream才算真正解决了这个瓶颈。真实项目里你会看到很多老服务还停留在HTTP/1.1升级到HTTP/2除了要配HTTPS证书之外还得注意服务端框架是否支持h2协议。连接复用不是银弹我踩过一个很典型的坑Docker拉镜像时偶尔报error response from daemon: Get https://registry-1.docker.io/v2/: net/http这其实就发生在客户端和Docker Registry之间的HTTPS连接上。用户拉镜像本质上是HTTP客户端到HTTPS服务器的请求中间如果走了代理代理又对连接做了长连接复用一旦代理侧连接超时或者TLS会话过期客户端还傻乎乎地复用旧连接就会报这种看似莫名其妙的网络错误。**排查思路很直接先确认代理环境变量再手动curl测一下Registry的连通性基本能定位一半以上的问题。**具体的我放在后面实操章节详细说。2. HTTPS到底做了什么一条加密隧道与一纸证书2.1 HTTPS在协议栈中的位置和核心加密思路HTTPS不是一种新协议它就是在HTTP下面垫了一层TLS传输层安全协议。数据从应用层HTTP往下走到TLS层时会被加密到达对端后再由TLS层解密还原成HTTP明文。这里有个重要的认知HTTPS在起点和终点都是明文只有中间传输过程是密文。这意味着你抓本机的包看到的是密文但服务器内部日志看到的是明文HTTP——所以别再问HTTPS为什么服务器日志还是明文这种问题了。那加密是怎么实现的呢现代密码学里有两类算法缺一不可对称加密加密和解密用同一个密钥速度快适合加密大量数据。常用的有AES高级加密标准在工作模式上AES-GCM因为同时提供机密性和完整性校验几乎是现在TLS的首选TLS 1.3里还支持ChaCha20-Poly1305在手机等没有AES硬件加速的设备上性能更稳。非对称加密加密和解密用一对不同的密钥公钥加密、私钥解密或者私钥签名、公钥验签。典型代表是RSA和ECC椭圆曲线密码。它速度慢不适合加密大数据但能解决一个对称加密解决不了的难题——两个从没见过面的人怎么安全地商量出一个共同的密钥。这里我必须强调一下非对称加密用来加密对称密钥只是最粗浅的理解。真正现代TLS的握手流程用的是密钥交换算法Key Exchange它让客户端和服务器各自持有一份随机私密信息通过公开交换一些参数最终推导出一个一致的会话密钥。就算攻击者全程旁听了交换的每个数据包也算不出这个会话密钥。这类算法的代表是ECDHE基于椭圆曲线的临时Diffie-Hellman它有个极重要的特性叫前向保密——即使服务器的长期私钥将来泄露了过去录制的加密流量也无法被解密。这也是为什么现在业界坚决不用单纯的RSA密钥交换的原因。2.2 TLS握手拆解客户端和服务器怎么互相信任TLS握手是整个HTTPS体系最精彩的部分也是日常排查问题绕不开的核心。我用TLS 1.2的完整流程来说因为很多老系统还在用第一步ClientHello客户端告诉服务器自己支持的TLS版本、加密套件列表和一个随机数。第二步ServerHello服务器从列表里挑一个双方都支持的加密套件返回自己的随机数和证书链。第三步证书验证客户端拿到服务器证书后要检查证书是否由可信CA签发、域名是否匹配比如访问example.com却拿到evil.com的证书直接报警、证书是否过期、是否被吊销。这一步是信任的关键也是攻击者最爱下手的地方。第四步密钥交换如果是ECDHE双方各自生成临时密钥对交换公钥然后用自己的私钥加上对方的公钥算出一个共同的预主密钥再通过一系列PRF伪随机函数推导出真正的会话密钥。第五步Finished双方用已协商的密钥加密握手消息的摘要发给对方验证。验证通过握手完成后面应用数据全部走对称加密。TLS 1.3做了很大的瘦身把密钥协商提前到第一轮交互里整个握手缩短到一次RTT往返延迟并且砍掉了所有不安全的旧算法。我在生产环境里的经验是能上TLS 1.3就上TLS 1.3性能和安全同时拿。证书链的验证是另一个高频翻车点。CA签发证书时通常不是直接用根CA签而是签给一个中间CA再由中间CA签给你的域名证书。客户端需要从叶子证书向上回溯到根CA中间任何一环缺失或格式不对验证就失败。很多人用Nginx配SSL只填了域名证书文件没把中间证书链串进去结果浏览器报错NET::ERR_CERT_AUTHORITY_INVALID。**正确做法是把叶子证书和中间证书按顺序拼成一个文件填给Nginx。**这个坑我在后面实操节里写具体命令。2.3 国产密码算法与合规场景聊到加密算法就绕不开国密。国内一些涉及金融、政务的系统审核要求用国产密码算法对应关系大概是SM2对标RSA/ECC做非对称加密和签名SM3对标SHA-256做哈希SM4对标AES做对称加密。在HTTPS场景里部署国密SSL一般是同时配置国密证书和国际证书根据客户端支持的算法协商使用哪一套。这个操作在Nginx里有专门的国密版编译模块不是改几行配置就能实现的需要提前跟运维确认环境。对多数普通Web应用来说标准TLS足够只有明确合规要求时再去趟国密这条河别没事自己加复杂度。3. 安全攻防HTTPS场景下真实存在的攻防博弈3.1 中间人攻击是如何发生的HTTPS解决了明文问题但攻击者并没有躺平他们换了个思路既然直接读密文读不了那就想办法让你跟攻击者建立TLS连接而不是跟真正的服务器建立连接。这就是中间人攻击MITM。攻击链条是这样的攻击者把自己插在客户端和服务器之间的网络链路上比如恶意WiFi、ARP欺骗、DNS劫持然后客户端发连接请求时攻击者冒充服务器攻击者同时再作为客户端去连接真正的服务器。于是客户端和攻击者之间是一条TLS隧道攻击者和服务器之间是另一条TLS隧道两条隧道的密钥攻击者都知道所有数据在他手里都变成明文。这在安全圈有个形象的叫法——MITM代理。问题来了客户端凭什么会信任攻击者正常逻辑下攻击者拿不出可信CA签给目标域名的证书客户端校验域名和证书签发者时就会报警。所以攻击者真正的攻击重点不在破解TLS而在让你接受一个假的证书。手段包括往系统信任区塞自己的根证书、利用用户点仍然继续的侥幸心理、或者做SSL剥离SSL Strip——先把用户的HTTPS请求降级成HTTP再自己去跟服务器走HTTPS这一招尤其阴险因为用户浏览器地址栏如果不加锁根本感知不到连接已经被劫持。我在课程里给团队做演示的时候最常用Charles和Fiddler这类抓包工具的HTTPS代理功能。原理跟攻击者一模一样把Charles的根证书导入到客户端的信任列表客户端去请求https://example.comCharles用自己签发的example.com证书跟客户端通信同时Charles用真正的证书去跟服务器通信。在客户端眼里这流量是合法HTTPS但你在Charles里看到的全是明文。这就是https明文捕获热词的来源。这里必须明确一个安全边界仅限你自己控制的设备、测试环境或者明确授权的安全测试。往别人的机器上静默装根证书那是违法的事别碰。3.2 防御视角证书固定、HSTS和证书透明度讲完攻击说防御。既然中间人攻击的核心是让你接受假证书那么防御就围绕怎么让假证书无效展开。第一招叫证书固定Certificate Pinning。客户端在代码里写死服务器的公钥指纹或者证书指纹就算系统信任列表里多了千百个CA只要对方拿出的证书指纹对不上立即断开连接。移动端安全要求高的App经常用这招它能有效防住根证书被植入这种场景。缺点也很明显证书轮换时如果没提前更新客户端会造成大面积连接失败所以现在更多人倾向用HPKPHTTP公钥固定或者干脆只固定公钥留出换证的缓冲期。第二招是HSTSHTTP严格传输安全。服务端返回一个Strict-Transport-Security响应头告诉浏览器未来一段时间内这个域名只许走HTTPS任何HTTP请求都自动转成HTTPS。这招直接废掉SSL剥离攻击。更彻底的是HSTS preload把域名提交到浏览器内置的强制HTTPS列表里第一次访问就强制HTTPS没有降级窗口期。我在自己的服务器上配了preload功能没问题但要注意preload一旦生效移除要等版本更新慎重。第三招是证书透明度Certificate TransparencyCT。CA每次签发的证书都必须写入公开可查的日志服务器域名所有者可以通过查看日志来发现自己域名被冒签了证书。Google Chrome要求所有证书必须带SCT签名证书时间戳没有就不认这就是CT在实际落地中的体现。3.3 测试与排障中的合法解密思路日常做测试怎么合法地拿HTTPS明文分两种情况。第一种是抓自己的客户端到自己的服务器还是那句话得是你自己控制的服务器。最干净的方式是用Wireshark配合环境变量SSLKEYLOGFILE让浏览器或客户端把每个会话的密钥明文写到指定文件Wireshark读这个文件就能直接解密TLS流量。Chrome和Firefox都支持启动命令行类似export SSLKEYLOGFILE/tmp/keys.log然后Wireshark的TLS协议设置里指定这个文件路径。这个方案能看全握手细节排查协议层问题非常高效。第二种是通过Charles或Fiddler这类代理抓包工具。它会安装自己的根证书到系统或者浏览器的信任区然后用中间人模式解密HTTPS。好处是界面友好移动端抓包也方便——手机WiFi代理指向电脑上的Charles端口再装上证书iOS和Android的请求一目了然。JVM客户端比如Java写的服务更特殊一点它不读操作系统的信任库需要把Charles证书导入到JDK的cacerts里用keytool命令操作后面我会给出具体命令。4. 实操从零配置一个生产可用的HTTPS服务4.1 申请证书和Nginx配置如果你没有特殊合规要求最推荐的方式是用Lets Encrypt免费证书配合certbot自动续期。一个非常常见的误解是免费证书不安全、不稳定恰恰相反Lets Encrypt用的是全自动签发和续期机制只要域名DNS没问题证书到期前会自动更新我生产环境跑了好几年没出过事。签发命令以Nginx Ubuntu为例sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d example.com -d www.example.comcertbot会自动改Nginx配置然后在你指定的webroot或者Nginx插件路径下生成证书文件。生成后关键要检查自动续期任务是否配置成功sudo certbot renew --dry-run真正的Nginx配置模板我一般这么写server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 1.1.1.1 valid300s; add_header Strict-Transport-Security max-age31536000; includeSubDomains always; root /var/www/html; index index.html; }几个容易被忽视的点fullchain.pem是叶子证书中间证书的合并文件千万别只填cert.pem否则公网浏览器各种报证书链不完整。ssl_ciphers那行我用的是TLS 1.2时代以来的主流强密码套件优先ChaCha20给了移动端很好的兼容性。ssl_session_cache配置会直接影响HTTPS握手性能没配的话每次新连接都要完整握手一遍TLS会话复用的优势就浪费了。ssl_stapling on是开启OCSP装订服务器自己把证书吊销状态查询结果发给客户端省去客户端去CA查询的额外往返。HTTP自动跳转HTTPS的配置也不复杂在80端口server块里加一行server { listen 80; server_name example.com; return 301 https://$host$request_uri; }配合上面HSTS头访问http://example.com会先被Nginx跳转到https://example.com浏览器记住HSTS后以后连这次跳转都省了直接请求HTTPS。4.2 用openssl和curl验证配置配置完了别急着上线先本地验证一遍。用openssl看证书链openssl s_client -connect example.com:443 -servername example.com -showcerts重点看输出里的这几项Certificate chain是否完整应该有2到3个证书Verify return code: 0 (ok)是不是显示正常。如果Verify return code不是0说明证书链有问题最常见的是unable to get local issuer certificate就是中间证书没带全。再看协议和加密套件用的什么curl -vI https://example.com 21 | grep -E SSL|TLS命令行下会显示SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384这就是TLS 1.3强加密套件状态很好。如果显示的是TLSv1.0或者CBC字样的老套件说明配置里有兼容老客户端的降级项建议检查ssl_protocols和ssl_ciphers。还有个小技巧用curl --http2 -I https://example.com看协议是不是HTTP/2如果返回200且显示h2说明HTTP/2多路复用已经启用。4.3 JMeter录制HTTPS脚本的完整步骤做性能测试或者接口测试的朋友经常需要在JMeter里录制HTTPS脚本这个操作比普通HTTP录制麻烦一点。踩过几次坑之后我总结了一套稳定流程先启JMeter里的HTTP代理服务器测试计划里右键添加非测试元件 - HTTP代理服务器端口设成8888目标控制器指向线程组。然后在JMeter的bin目录下找到ApacheJMeterTemporaryRootCA.crt这个证书要导入到你的Java环境里否则JMeter作为代理转发HTTPS请求时目标服务器不认它。Windows导入命令keytool -import -alias jmeter -file ApacheJMeterTemporaryRootCA.crt -keystore C:\Program Files\Java\jdk-17\lib\security\cacertsmacOS/Linux 同理注意cacerts路径要根据JDK版本变化。浏览器也要信任这个根证书Chrome打开chrome://settings/certificates导入到受信任的根证书颁发机构。接着浏览器设置HTTP代理指向127.0.0.1:8888开始录制前先清空JMeter的代理记录录制完关闭代理。整个过程如果发现录制到的还是http://明文URL检查是不是代理设置没生效或者浏览器在走系统代理而JMeter监听的是本地端口。还有一个我很早就踩过的坑JMeter 5.x以后默认不自动保存根证书如果你删了临时CA以后录制会全部报SSL握手失败重新生成一份CA并把新证书重新导一遍就行。5. 真实场景问题排查我遇到的几个高频报错5.1 Docker拉镜像报错Get https://registry-1.docker.io/v2/: net/http这个报错几乎每个用Docker的人都遇到过而且原因五花八门。我遇到过的典型场景有两种。第一种是网络环境里走了代理但代理配置不允许访问国外的Registry或者代理规则把registry-1.docker.io排除掉了。排查时先看Docker的代理配置systemctl show docker --propertyEnvironment或者看~/.docker/config.json里的proxies配置。确认有代理后用curl -x 代理地址 -I https://registry-1.docker.io/v2/测试一下真实连通性很容易看出问题。第二种是TLS握手本身失败可能因为本地的根证书缺失或者系统时间不对。如果系统时间比实际时间差太多TLS证书校验必然失败Docker会报x509: certificate signed by unknown authority或类似错误。先date检查时间再用openssl s_client -connect registry-1.docker.io:443手动验证证书链。很多故障排除到最后其实是系统时钟的问题这类问题在刚克隆的虚拟机里特别常见。还有一个更大路的方案配置镜像加速器。国内有大量公开的Docker镜像加速地址原理是它替你从上游拉取镜像再转存一份你把Docker的registry-mirrors配置指向它就能缓解连通性问题。注意**镜像加速只能缓解拉取慢和连接超时如果问题出在证书校验或代理截断上加速器也救不了还是要回到原节点排查。**建议排查顺序是时间 - 代理 - DNS - 证书 - 换源。5.2 IDEA报错Cannot start internal HTTP server这个报错在IntelliJ IDEA和Android Studio里都很常见。JetBrains IDE内置了一个HTTP服务器端口通常是63342用来提供本地预览、插件通信和调试功能。启动失败时十有八九是端口被占用。Windows上查端口占用netstat -ano | findstr 63342 taskkill /F /PID 进程号macOS/Linuxlsof -i :63342 kill -9 进程号杀掉进程后重启IDEA基本就好。如果还不行看看是不是开了系统代理环境变量IDE内部服务器启动时要绑定本地端口当系统代理被配置成全局代理时IDE可能会把连接请求往代理上发导致握手失败。在IDEA设置里找到HTTP Proxy改成Auto-detect proxy settings或者No proxy再试。5.3 Chrome提示当前设备加密等级较低怎么办这个提示一度让很多人困惑它跟Chrome的受保护密码同步功能有关。Chrome检测到设备没有启用完整的磁盘加密比如Windows的BitLocker/设备加密就认为密码库存在被物理读取的风险于是给出提示。解决方法很直接Windows系统设置里搜索设备加密或BitLocker把系统盘的设备加密打开。要启用设备加密前提是主板BIOS里开启了TPM和Secure Boot很多老机器的TPM默认没开需要进BIOS打开。开完BitLocker再重启Chrome提示就消失了。这台设备如果只是临时用也可以直接忽略它不影响正常上网。5.4 STM32这类嵌入式设备访问HTTPS怎么破嵌入式场景很特殊因为STM32F103C8T6这种单片机资源极其紧张RAM只有20KB左右Flash也就64KB。完整跑一套TLS库压力巨大。别慌一般就两条路。第一条路用带TLS硬件加速模组。比如ESP32内置硬件AES和TLS加速配合mbedTLSARM出品的轻量TLS库可以跑HTTPS或者用AT指令模组比如乐鑫的ESP-AT固件单片机发AT命令模组自己处理TLS握手对MCU几乎零负担。我实际项目里更推荐这种方式省心。第二条路如果一定要在STM32F103上跑那就得对mbedTLS做深度裁剪。关键点在于证书把服务器证书或CA证书编译进固件关掉证书链在线验证只做固定指纹校验可以省掉一大截内存。需要参考 mbedTLS 官方文档的配置宏选项来裁剪比如关闭MBEDTLS_SSL_MAX_FRAGMENT_LENGTH以外的所有扩展调低握手缓冲区大小。注意别关掉所有校验否则和明文差不多至少要保留最基本的证书日期和域名检查。再提醒一个容易忽视的点**STM32上跑HTTPS时间同步必须先做。**TLS证书有效期校验依赖客户端系统时间没有RTC或者没做NTP同步证书验证直接失败。有同学就是忘了这一条排了一下午最后发现是时间不对。5.5 HTTP状态码快速查阅最后列一份常用HTTP状态码速查表我在定位问题时经常要翻放到这里方便你收藏。状态码含义典型场景200 OK请求成功正常响应页面能开301 Moved Permanently永久重定向HTTP跳HTTPS、域名迁移302 Found临时重定向登录后跳转、短链服务304 Not Modified资源未修改浏览器本地缓存生效400 Bad Request请求格式错误参数缺失、JSON语法错误401 Unauthorized未认证没带Token访问受限接口403 Forbidden无权限登录了但没权限404 Not Found资源不存在URL写错或者被删了429 Too Many Requests请求太频繁触发限流500 Internal Server Error服务器内部错误后端代码异常502 Bad Gateway网关错误Nginx后面服务挂了503 Service Unavailable服务不可用服务过载或正在维护504 Gateway Timeout网关超时上游服务响应太慢排查状态码有个经验**3xx先查重定向逻辑4xx大部分是客户端问题5xx是服务器问题。**遇到502/504第一时间去看上游服务的健康状态和超时配置别在Nginx配置上瞎猜遇到429检查是不是自己的请求脚本忘记控制QPS了。这个表配合TLS握手错误一起看线上故障大部分能快速定位到具体环节。我在实际工作里最大的体会是HTTP到HTTPS的升级不是简单换个端口就完事它牵扯到证书管理、加密套件选型、性能调优、客户端兼容性还有一整套安全防御的思维。尤其是证书链和代理这两块踩坑率极高但只要你理解握手过程中每一步在干什么排障就变得很机械反而不难了。希望这篇能帮你省下一些踩坑的时间。
返回列表