考虑开启SRI防止七牛CDN HTTPS劫持

最近我在使用 Android 上的 Chrome 浏览器访问博客页面时发现一个奇怪的问题:博客页面底部有一个悬浮的叉,但又没有显示任何实际的内容。赶紧用 Chromebook 打开博客网页,将 User Agent 切换成 Android Chrome,这时可以看到网页里无端多了一个 iframe,该 iframe 地址为 http://dbcpm.com/locate_1/jiwei_MBpt.html,如下图所示:

由于我确定博客 VPS 后台并没有被入侵,因此初步估计是网页被万恶的运营商给劫持了,但又一想我的博客已经启用了全站 HTTPS,按说不会轻易遇到这种问题了。马上看看 Chrome 浏览器上的地址栏标志,果然没有小绿锁了,浏览器控制台里也有 Mixed Content 报错:

可以看到浏览器拒绝加载 HTTP 的 JavaScript 文件,接着看看七牛 HTTPS 地址返回的数据是否正确:

这下立马发现返回的 JavaScript 代码不对了,开头是插入了 HTTP 形式的 JavaScript 文件地址,后面则看起来明显是广告一类的程序代码了。

难道是运营商做了 DNS 劫持导致我使用的七牛 CDN HTTPS 域名 dn-zohead.qbox.me 解析到了不正确的地址?如果是这样按说应该会出现 SSL 证书错误的,而且运营商只是为了插入广告代码而去搞 TLS 中间人攻击之类的感觉也不太合理。

考虑到另一种可能就是七牛 CDN 在从我的博客 VPS 地址回源的时候由于 DNS 污染之类的问题请求到了错误的数据,导致 HTTPS CDN 返回的数据也不对,这样还是登录到七牛后台管理,查看被劫持的 main.js 文件内容是否正确,下载下来对比却发现和我的博客源站内容是一致的,并没有被污染。

多次测试之后我发现如果再刷新页面七牛 HTTPS 劫持的问题可能又没了,而且在新标签页中打开被劫持的 HTTPS JavaScript 路径又能返回正确的数据了,这个时候通过 Chrome 调试工具看到的 HTTP 请求响应结果是这样的:

而被劫持的 HTTP 请求响应结果则是:

可以看到返回的 HTTP 头信息完全不同,而且多次刷新之后发现被劫持的 HTTPS 数据基本都来自 211.142.22.13 这台七牛的 CDN 服务器。查询之后发现这个 IP 地址是山西移动的(刚好我使用的是移动宽带),看起来只要浏览器是从 211.142.22.13 这台 CDN 服务器请求数据很可能得到的是被劫持的 JavaScript 代码,而如果七牛的 qbox.me 域名解析到的是其它 CDN 服务器地址则数据可能是正常的。

为了更好的重现这个问题,我们可以在 Linux 下先修改系统 hosts 文件使七牛的 qbox.me HTTPS 域名使用进行劫持操作的服务器,然后使用 wget 命令伪装 Android 移动设备以 HTTPS 地址从这台服务器请求 JavaScript 数据:

(trusty)zzm@localhost:~$ wget --referer="https://zohead.com/" --user-agent="Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36" https://dn-zohead.qbox.me/test.js
--2016-05-27 00:30:37--  https://dn-zohead.qbox.me/test.js
Resolving dn-zohead.qbox.me (dn-zohead.qbox.me)... 211.142.22.13
Connecting to dn-zohead.qbox.me (dn-zohead.qbox.me)|211.142.22.13|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2391 (2.3K) 1
Saving to: ‘test.js’

100%[===========================================================================================================>] 2,391       --.-K/s   in 0.002s  

2016-05-27 00:30:38 (1.18 MB/s) - ‘test.js’ saved [2391/2391]

上面使用 wget 的 --user-agent 参数伪装 Android Chrome 浏览器,使用 --referer 参数伪装从我的博客源站发起请求,而 test.js 同样是一个实际不存在的 JavaScript 地址,我们可以看看返回的数据:

(trusty)zzm@localhost:~$ head test.js 
var osrc ="http://dn-zohead.qbox.me/test.js";osrc+=(osrc.indexOf('?')>0?'&':'?')+'_t='+(new Date().getTime());document.write('<script type="text/javascript" src="'+osrc+'"></'+'script>');function withjQuery(callback){if(typeof(jQuery)=="undefined"){var script=document.createElement("script");scrip ......

很明显这下又得到了被篡改的数据,我们可以试试把 HTTPS 地址改成一个实际存在的 JavaScript 文件并多次运行,你会发现会有一定的概率返回被篡改的数据,有的时候也能返回正确的数据。

另外如果你去掉 --user-agent 参数或者 --referer 参数可能也能得到正确的数据或者正确的 404 报错(对于不存在的 JavaScript 地址),因此我想到可能是劫持方为了不让自己这种龌龊的行径被很容易的发现,而对劫持做了一些限制,如果是直接请求地址(不通过源站 referer 请求)或者通过 PC 端浏览器请求地址则基本上都返回正确的数据。

这下我就可以单独确认 211.142.22.13 这个地址到底是不是真正的七牛服务器了,我们可以修改系统 hosts 文件,例如将我的七牛 HTTPS 域名 dn-zohead.qbox.me 直接改为 211.142.22.13 ,然后通过 Chrome 浏览器访问一个七牛服务器上不存在的 JavaScript 地址,确认 HTTPS 证书是否正确。

提示

从上面 wget 命令的测试结果来看,劫持方可能做了 User Agent 和 Referer 的限制,因此直接访问地址可能并不能看到劫持效果。
这就需要将 Chrome 浏览器 User Agent 改为 Android Chrome,另外可以考虑在控制台中输入命令在网页上创建一个链接,并点击链接实现 Referer 跳转的效果,例如:

var aaa = document.createElement('a');
aaa.innerHTML = 'testlink';
aaa.href = 'https://dn-zohead.qbox.me/ccc8.js'; document.body.appendChild(aaa);

先看看进行劫持操作的 211.142.22.13 CDN 服务器的 HTTPS 证书:

看起来 SSL 服务器证书是正确的,而且从截图的内容也可以看到返回的 JavaScript 代码也明显是被篡改过的。

然后再修改 hosts 文件去掉添加的域名地址条目,重新访问同样不存在的 JavaScript 地址,以确认正确的七牛 CDN 服务器的 HTTPS 证书:

可以发现正常的七牛 CDN 服务器对于不存在的文件是可以正确返回 Document not found 错误的,而进行劫持操作的 CDN 服务器则可能会返回篡改过的 JavaScript 代码,而最要命的是这两个服务器的 HTTPS 证书是完全一致的。

而我使用 HTTPS 访问博客时由于 Mixed Content 问题会导致博客页面时有些功能不正常,而且本该被篡改插入的广告反倒没显示出来,用 Android Chrome 浏览器以 HTTP 方式访问博客多次刷新之后可以看到被劫持插入页面的广告,按说也可以根据 CNZZ 统计代码追踪看看,不过这个就没太大兴趣了:

至此七牛 CDN HTTPS 劫持的问题基本可以明确了:

  • 重点针对 JavaScript 文件;
  • 并不是七牛 CDN 上 DNS 污染而回源数据不正确导致;
  • 并不是所有七牛 CDN 地址都会返回篡改的数据;
  • 重点针对移动浏览器,移动网络环境下更加明显;
  • 对劫持做了一定的限制,防止被轻易发现;
  • 进行劫持操作的服务器使用的是正确的七牛 HTTPS 证书。

至于这种恶意 CDN HTTPS 劫持到底是七牛内部人士所为,还是七牛的 HTTPS 证书泄漏被运营商利用,还需要进一步的确认。提交工单之后七牛客服人员的初步解释是:

  • qbox.me 域名被大量客户使用比较不稳定,建议迁移到 HTTPS 自定义域名,而 HTTPS 自定义域名是收费的;
  • 使用 qnssl.com HTTPS 自定义域名的话则有做回源的验证,并且节点相对较多,应该不会有劫持情况的发生。

然而我查看了七牛 CDN 后台数据之后还是基本可以确认并没有回源数据被污染的情况,因此七牛给的答复并不能让我满意。

由此也可以看出在这片神奇的土地上,我们的网络环境除了要面临 GFW 这个几乎众所周知的阻碍在其它方面又到底是如何的恶劣,网站主们即使是开启了全站 HTTPS 也难以幸免。就算是七牛所有 CDN 服务器数据都是干净的,也不能保证网络运营商不在中间干点 DNS 污染、TLS 攻击之类的坏事。

如果要解决这种问题我初步想到的就是为博客开启 Subresource Integrity(SRI)安全检查功能,虽然目前支持 SRI 功能的浏览器(目前主要是 Chrome 和 Firefox)并不多,但其还是可以帮助 Web 开发者尽量避免各种网站数据可能被第三方篡改的情况。

SRI 的相关介绍可以参考(第二篇中文文章介绍的比较详细):

由于 SRI 需要对网页中所有请求外部资源的地方进行修改,一个个手工通过 openssl 命令和修改网页来做实在比较麻烦。

对于 WordPress 博客已经有人实现了 SRI 资源管理的插件 Subresource Integrity (SRI) Manager,该插件可以自动为 WordPress 博客中引用的资源添加 SRI SHA-256 校验值,这样可以减少博客被各种 HTTP、HTTPS 中间人劫持攻击的机率。不过目前还是准备先测试看看此插件稳定性如何,后续再考虑为博客整体开启 SRI 功能咯。