2022 年 4 月 25 日

点击劫持攻击

“点击劫持”攻击允许恶意页面代表访问者点击“受害网站”。

许多网站都曾被这种方式攻击,包括 Twitter、Facebook、Paypal 等网站。当然,它们都已修复。

想法

这个想法非常简单。

以下是 Facebook 如何被点击劫持的

  1. 访问者被诱骗到恶意页面。方式并不重要。
  2. 该页面上有一个看似无害的链接(例如“现在致富”或“点击这里,非常有趣”)。
  3. 在该链接之上,恶意页面放置了一个透明的<iframe>,其src来自 facebook.com,并且“点赞”按钮正好位于该链接之上。通常这是通过z-index实现的。
  4. 在试图点击链接时,访问者实际上点击了按钮。

演示

以下是恶意页面示例。为了更清晰地说明,<iframe> 设置为半透明(在真正的恶意页面中,它是完全透明的)。

<style>
iframe { /* iframe from the victim site */
  width: 400px;
  height: 100px;
  position: absolute;
  top:0; left:-20px;
  opacity: 0.5; /* in real opacity:0 */
  z-index: 1;
}
</style>

<div>Click to get rich now:</div>

<!-- The url from the victim site -->
<iframe src="/clickjacking/facebook.html"></iframe>

<button>Click here!</button>

<div>...And you're cool (I'm a cool hacker actually)!</div>

攻击的完整演示

结果
facebook.html
index.html
<!DOCTYPE HTML>
<html>

<body style="margin:10px;padding:10px">

  <input type="button" onclick="alert('Like pressed on facebook.html!')" value="I LIKE IT !">

</body>

</html>
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>

  <style>
    iframe {
      width: 400px;
      height: 100px;
      position: absolute;
      top: 5px;
      left: -14px;
      opacity: 0.5;
      z-index: 1;
    }
  </style>

  <div>Click to get rich now:</div>

  <!-- The url from the victim site -->
  <iframe src="facebook.html"></iframe>

  <button>Click here!</button>

  <div>...And you're cool (I'm a cool hacker actually)!</div>

</body>
</html>

这里我们有一个半透明的 <iframe src="facebook.html">,在示例中我们可以看到它悬停在按钮上。点击按钮实际上是点击了 iframe,但用户无法看到,因为 iframe 是透明的。

因此,如果访问者已在 Facebook 上授权(通常会开启“记住我”),那么它会添加一个“点赞”。在 Twitter 上,这将是一个“关注”按钮。

以下是相同的示例,但更接近现实,<iframe> 设置为 opacity:0

结果
facebook.html
index.html
<!DOCTYPE HTML>
<html>

<body style="margin:10px;padding:10px">

  <input type="button" onclick="alert('Like pressed on facebook.html!')" value="I LIKE IT !">

</body>

</html>
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>

  <style>
    iframe {
      width: 400px;
      height: 100px;
      position: absolute;
      top: 5px;
      left: -14px;
      opacity: 0;
      z-index: 1;
    }
  </style>

  <div>Click to get rich now:</div>

  <!-- The url from the victim site -->
  <iframe src="facebook.html"></iframe>

  <button>Click here!</button>

  <div>...And you're cool (I'm a cool hacker actually)!</div>

</body>
</html>

我们只需要将恶意页面上的 <iframe> 定位到按钮正好位于链接上的位置。这样,当用户点击链接时,实际上是点击了按钮。这通常可以通过 CSS 实现。

点击劫持针对的是点击,而不是键盘

攻击只影响鼠标操作(或类似操作,例如移动设备上的点击)。

键盘输入很难重定向。从技术上讲,如果我们要攻击一个文本字段,我们可以将 iframe 定位到文本字段相互重叠的位置。因此,当访问者尝试将焦点放在页面上看到的输入框时,实际上是将焦点放在了 iframe 内部的输入框上。

但问题是,访问者输入的所有内容都会被隐藏,因为 iframe 是不可见的。

当用户无法看到新字符在屏幕上打印时,他们通常会停止输入。

老式防御(弱)

最古老的防御方法是使用一些 JavaScript 代码来禁止在框架中打开页面(称为“框架破坏”)。

它看起来像这样

if (top != window) {
  top.location = window.location;
}

也就是说:如果窗口发现它不在顶部,它会自动将自己设置为顶部。

这不是可靠的防御方法,因为有很多方法可以绕过它。让我们介绍一些方法。

阻止顶部导航

我们可以在 beforeunload 事件处理程序中阻止更改 top.location 引起的转换。

顶层页面(包含页面,属于黑客)设置了一个阻止处理程序,如下所示

window.onbeforeunload = function() {
  return false;
};

iframe尝试更改top.location时,访问者会收到一条消息,询问他们是否要离开。

在大多数情况下,访问者会回答否,因为他们不知道iframe的存在 - 他们只能看到顶层页面,没有理由离开。所以top.location不会改变!

实际操作

结果
iframe.html
index.html
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>

  <div>Changes top.location to javascript.info</div>

  <script>
    top.location = 'https://javascript.js.cn';
  </script>

</body>

</html>
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">

  <style>
    iframe {
      width: 400px;
      height: 100px;
      position: absolute;
      top: 0;
      left: -20px;
      opacity: 0;
      z-index: 1;
    }
  </style>

  <script>
    function attack() {

      window.onbeforeunload = function() {
        window.onbeforeunload = null;
        return "Want to leave without learning all the secrets (he-he)?";
      };

      document.body.insertAdjacentHTML('beforeend', '<iframe src="iframe.html">');
    }
  </script>
</head>

<body>

  <p>After a click on the button the visitor gets a "strange" question about whether they want to leave.</p>

  <p>Probably they would respond "No", and the iframe protection is hacked.</p>

  <button onclick="attack()">Add a "protected" iframe</button>

</body>
</html>

沙箱属性

sandbox属性限制的一件事是导航。沙箱化的iframe不能更改top.location

因此,我们可以使用sandbox="allow-scripts allow-forms"添加iframe。这将放宽限制,允许脚本和表单。但我们省略了allow-top-navigation,因此更改top.location是被禁止的。

以下是代码

<iframe sandbox="allow-scripts allow-forms" src="facebook.html"></iframe>

还有其他方法可以绕过这种简单的保护。

X-Frame-Options

服务器端标头X-Frame-Options可以允许或禁止在框架中显示页面。

它必须完全作为HTTP标头发送:如果在HTML<meta>标签中找到,浏览器将忽略它。因此,<meta http-equiv="X-Frame-Options"...>将不起作用。

标头可能具有3个值

DENY
永远不要在框架中显示页面。
SAMEORIGIN
如果父文档来自同一来源,则允许在框架中显示。
ALLOW-FROM domain
如果父文档来自给定域,则允许在框架中显示。

例如,Twitter 使用X-Frame-Options: SAMEORIGIN

以下是结果

<iframe src="https://twitter.com"></iframe>

根据您的浏览器,上面的iframe要么为空,要么提醒您浏览器不允许该页面以这种方式导航。

显示带有禁用功能

X-Frame-Options标头有一个副作用。其他网站将无法在框架中显示我们的页面,即使他们有充分的理由这样做。

因此,还有其他解决方案……例如,我们可以用一个<div>覆盖页面,该<div>具有样式height: 100%; width: 100%;,以便它可以拦截所有点击。如果window == top或我们发现不需要保护,则应删除该<div>

类似于以下内容

<style>
  #protector {
    height: 100%;
    width: 100%;
    position: absolute;
    left: 0;
    top: 0;
    z-index: 99999999;
  }
</style>

<div id="protector">
  <a href="/" target="_blank">Go to the site</a>
</div>

<script>
  // there will be an error if top window is from the different origin
  // but that's ok here
  if (top.document.domain == document.domain) {
    protector.remove();
  }
</script>

演示

结果
iframe.html
index.html
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">

  <style>
    #protector {
      height: 100%;
      width: 100%;
      position: absolute;
      left: 0;
      top: 0;
      z-index: 99999999;
    }
  </style>

</head>

<body>

<div id="protector">
  <a href="/" target="_blank">Go to the site</a>
</div>

<script>

  if (top.document.domain == document.domain) {
    protector.remove();
  }

</script>

  This text is always visible.

  But if the page was open inside a document from another domain, the div over it would prevent any actions.

  <button onclick="alert(1)">Click wouldn't work in that case</button>

</body>
</html>
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
</head>
<body>

  <iframe src="iframe.html"></iframe>

</body>
</html>

Samesite cookie 属性

samesite cookie 属性也可以防止点击劫持攻击。

具有此属性的 cookie 仅在直接打开网站时才会发送,而不是通过框架或其他方式发送。有关更多信息,请参阅章节Cookies, document.cookie

如果网站(例如 Facebook)在其身份验证 cookie 上具有samesite属性,如下所示

Set-Cookie: authorization=secret; samesite

…那么当 Facebook 在来自另一个网站的 iframe 中打开时,此 cookie 不会被发送。因此,攻击将失败。

当不使用 cookie 时,samesite cookie 属性将不起作用。这可能允许其他网站轻松地在 iframe 中显示我们的公共、未经身份验证的页面。

但是,这也可能在少数情况下允许点击劫持攻击。例如,一个通过检查 IP 地址来防止重复投票的匿名投票网站,仍然容易受到点击劫持的攻击,因为它没有使用 cookie 对用户进行身份验证。

总结

点击劫持是一种“欺骗”用户点击受害者网站而无需他们知晓的方式。如果存在重要的点击激活操作,这将非常危险。

黑客可以在消息中发布指向其恶意页面的链接,或通过其他方式诱使访问者访问其页面。有很多变体。

从一个角度来看,攻击“并不深入”:黑客所做的只是拦截一次点击。但从另一个角度来看,如果黑客知道点击后会出现另一个控件,那么他们可能会使用狡猾的消息来迫使用户也点击它们。

这种攻击非常危险,因为当我们设计 UI 时,通常不会预料到黑客可能会代表访问者点击。因此,漏洞可能出现在完全意想不到的地方。

  • 建议在不打算在框架内查看的页面(或整个网站)上使用X-Frame-Options: SAMEORIGIN
  • 如果我们希望允许我们的页面显示在 iframe 中,但仍然保持安全,请使用覆盖<div>
教程地图

评论

在评论之前请阅读…
  • 如果您有改进建议,请提交 GitHub 问题或拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用<code>标签,对于多行代码,请将它们包装在<pre>标签中,对于超过 10 行的代码,请使用沙盒(plnkrjsbincodepen…)