📘 第十一章 Web的攻击技术
📄 一句话概括

一个全透明的 iframe 盖在目标页面上,用户以为点的是「领取奖品」,其实是银行设置页里的按钮。 Demo 里那行 iframe 一共 135 字节。 防御不在业务代码里,在响应首部:X-Frame-Options 或 CSP 的 frame-ancestors。

📝 本节和前面几节的关系

上一节是「替你发请求」,这一节是「骗你亲自点」。用户的每一次点击都是真的。

一、透明的那一层

攻击页面做两件事:用一个 opacity: 0 的 iframe 把目标站点整个铺满,再在自己这一层放上诱饵按钮, 位置正好压在 iframe 里那个真按钮上。 用户看到的是诱饵,点下去落在 iframe 里。浏览器不会告诉他这一下点到了另一个文档。 Demo 里那行 iframe 一共 135 字节——这一招的全部成本就这么点。 和 XSS 的区别值得记牢:XSS 要往页面里塞脚本,得先找到注入点; 点击劫持一行脚本都不需要,目标站点也不必有任何漏洞。 这一条特别值得记住:它不需要用户做错任何事,也不需要服务端有漏洞。 服务端一切正常,用户也确实点了他看到的按钮——只是他心里想的是另一个按钮。

二、为什么修法在首部里

被利用的不是某个业务逻辑,而是「浏览器允许一个页面把另一个页面嵌进来」这件事。 所以要关的也是这个开关。 X-Frame-Options: DENY 完全不许被嵌;SAMEORIGIN 只许同源嵌。 它是老方案,兼容性极好,但只能表达这两三档,列不出具体的源。 CSP 的 frame-ancestors 更细,可以列一串允许的源,none 等于 DENY。 两个都发时,支持 CSP 的浏览器按 CSP 判。 一个坑:frame-ancestors 只在响应首部里有效,写进 HTML 的 <meta> 不管用。 另一个坑:X-Frame-Options 只防「被嵌」,不防 XSS,两件事要分别做。

三、实测四种设置

Demo 对一个页面分别用四种设置发响应,看响应首部里有没有那一行、值是什么。 结果:什么都不设的时候一条相关首部都没有; DENY、CSP 的 frame-ancestors、SAMEORIGIN 三种都发出去了,值也对。 这里必须说清楚:最后一列「能不能被套」是按规范推的判定,不是渲染结果。 本机没有浏览器,我没法真的把一个页面套进另一个页面看它拦不拦。 能确定的是两件事:响应首部确实发出去了(Demo 打出来的那几行就是); 而浏览器按规范遇到这两个首部就会拒绝渲染。这两条加起来,结论才站得住。

四、动手 Demo:四种设置,只有一种漏着

本机起一个服务端,用四种不同的首部设置返回同一个页面,逐行看响应里到底带了什么首部。 再算一下攻击页面里那行 iframe 有多长。

SETTINGS = {
    u'none': [],
    u'DENY': [(u"X-Frame-Options", u"DENY")],
    u'CSP':  [(u"Content-Security-Policy", u"frame-ancestors 'none'")],
    u'SAME': [(u"X-Frame-Options", u"SAMEORIGIN")],
}

IFRAME = (u'<iframe src="https://bank.example/settings" '
          u'style="opacity:0;position:absolute;top:0;left:0;'
          u'width:100%;height:100%;z-index:2"></iframe>')

实跑输出:

--------------------------------------------------------------------
点击劫持:一个透明层盖在按钮上
--------------------------------------------------------------------
  攻击者的做法:在自己的页面里用一个全透明 iframe 盖住目标站点,
  再在同一个位置放一个「领取奖品」按钮。用户以为点的是奖品,其实点的是 iframe 里的按钮。

  攻击页面的关键那一行(135 字节):
    <iframe src="https://bank.example/settings" style="opacity:0;position:absolute;top:0;left:0;width:100%;height:100%;z-index:2"></iframe>

  服务端设置                  响应首部                                         能不能被套
  什么都不设                  (没有相关首部)                                     能
  X-Frame-Options        x-frame-options: DENY                        不能
  CSP frame-ancestors    content-security-policy: frame-ancestors 'no 不能
  X-Frame-Options 只许同源   x-frame-options: SAMEORIGIN                  别站不能

  注意最后一列是**按规范推的判定**,不是真实渲染结果——本机没有浏览器,
  我没法把一个页面真套进另一个页面里看它拦不拦。
  能确定的是响应首部发出来了,而浏览器遇到这两个首部就会拒绝渲染。

  这一条特别值得记住:它不需要用户做错任何事,也不需要服务端有漏洞。

输出那张四行表里,只有第一行是「能」——什么都不设时,浏览器允许被嵌。 后三行是两种首部的三种写法,判定分别是「不能」「不能」「别站不能」。 每行打出来的首部名和值都跟设置一致。 最后一列是按规范推的判定,不是渲染结果。 本机没有浏览器,所以「拦不拦」我只能给推断,给不了实测——这一点我不想含糊过去。 那行 iframe 量出来 135 字节。 攻击者的整个页面加起来也不过几百字节,不需要脚本、不需要服务器、不需要知道目标站有什么漏洞。 这就是它为什么被列进「不需要漏洞的漏洞」。

30 秒实验

  1. 打开开发者工具,看一个银行或邮箱站点的响应首部里有没有 X-Frame-Options。
  2. 本地写两个 HTML,一个用 iframe 嵌另一个,看显示出来是什么。
  3. 想一想:为什么「跳转前给一个确认页」对点击劫持没用?

常见误区

  1. 「用户点的是他自己看到的按钮」——他看到的是诱饵,点的是下面那一层。
  2. 「我们站点没有 XSS,所以不怕」——这一招不需要站点有漏洞。
  3. 「在 HTML 里写个 meta 就够了」——frame-ancestors 只认响应首部。
自测三问(点开对答案) 1. 点击劫持是怎么实现的? —— 用全透明 iframe 把目标页面铺满,再在同一个位置压一个诱饵按钮。 2. 为什么它不需要目标站点有漏洞? —— 被利用的是「浏览器允许嵌入」这件事,业务代码一切正常。 3. 防它的两个首部是什么? —— `X-Frame-Options` 和 CSP 的 `frame-ancestors`。

前两种都还在「人」这一侧。下一种不骗任何人,直接把服务拖到不能响应。

相关笔记