绕过Razer基于DOM的XSS补丁可以教给我们什么

绕过Razer基于DOM的XSS补丁可以教给我们什么

目录导航

最近,当我与Linus Särud聊天时,我发现并报告给 Razer 漏洞披露计划的一个老故事重新浮出水面。早在 2017 年,我发现了一段 JavaScript 代码,deals.razerzone.com它在用户登录后处理重定向。

// let rurl = document.location.href;
if (razerUserLogin) {
  rurl = rurl.split("rurl=")[1];
  location.href = decodeURIComponent(rurl);
}

代码从rurlGET 参数中提取值,并将用户重定向到该 GET 参数的值。例如,https://deals.razerzone.com/?rurl=https://deals.razerzone.com/settings将重定向到https://deals.razerzone.com/settings.

let rurl =
  "https://deals.razerzone.com/?rurl=https://deals.razerzone.com/settings";
rurl.split("rurl=");
//=> [ 'https://deals.razerzone.com/?', 'https://deals.razerzone.com/settings' ]
rurl.split("rurl=")[1];
//=> 'https://deals.razerzone.com/settings'

除了由于缺乏对重定向端点 ( rurl) 的验证而导致的明显的开放重定向之外,此代码还容易受到基于 DOM 的 XSS 的攻击。

将该window.location.href属性设置为javascript:协议 URI 将在目标 Web 应用程序的上下文中执行 JavaScript 代码。一些简单的事情https://deals.razerzone.com/?rurl=javascript:alert(document.domain)会提示用户显示当前页面的document.domain.

绕过Razer基于DOM的XSS补丁可以教给我们什么

Razer 尝试使用以下if语句修补漏洞。

// let rurl = document.location.href;
// let siteURL = 'https://deals.razerzone.com';
if (razerUserLogin) {
  rurl = rurl.split("rurl=")[1];
  rurl = decodeURIComponent(rurl);
  if (
    rurl.indexOf(siteURL) > -1 &&
    rurl.split("://")[1].split("/")[0] === siteURL.split("://")[1].split("/")[0]
  ) {
    location.href = rurl;
  }
}

作者注:在继续阅读之前,我鼓励读者尝试代码并确定是否可以绕过验证。请随时用您的旁路回复此推文。

浏览上面的代码可能会引发读者思考为什么开发人员会编写这样的代码。大多数关于良好编码实践的文献都会提到“好的代码不应该有任何惊喜”。更重要的是,应该以某种形式记录一段重要代码(例如上面突出显示的代码)的目的(“为什么?”)。更好的是:如果可能,代码应该记录自己

绕过Razer基于DOM的XSS补丁可以教给我们什么

Razer 尝试的补丁在所有帐户上都失败了。为什么要手动解析rurl而不是依赖内置URLAPI?所有嵌套split()方法都从什么中提取rurl

在执行代码审查时,我希望更好地了解开发人员的想法。换句话说,我们需要确定上面代码的用途并回答“为什么?”

  1. rurl.indexOf(siteURL) > -1在某种意义上模糊匹配用户提供的重定向 URL ( rurl) 以确定可信 URL ( siteURL) 是否存在于字符串中。开发人员试图回答:trustedsiteURL是 user-supplied 的子字符串rurl吗?
  2. rurl.split("://")[1].split("/")[0]尝试从用户提供的重定向 URL 中提取主机名,并将其与来自受信任的主机名进行比较siteURLrurl.split("://")[1]应该删除 URL 的协议方案部分(例如https:),并.split("/")[0]丢弃显示主机名的 URL 路径。
let rurl = "https://example.com/settings";
rurl.split("://");
//=> [ 'https', 'example.com/settings' ]
rurl.split("://")[1];
//=> 'example.com/settings'
rurl.split("://")[1].split("/");
//=> [ 'example.com', 'settings' ]
rurl.split("://")[1].split("/")[0];
//=> 'example.com'

开发人员似乎正在尝试确定受信任的 URL 是否存在rurl以及其中的主机名是否rurl与其受信任的主机 ( deals.razerzone.com) 匹配。

不幸的是,可以通过多种方式绕过此验证。indexOf()签入 (1.) 只需要出现https://deals.razerzone.comrurl; 不严格在字符串的开头。步骤 (2.) 将在第一次出现://. 所以rurl还是可以开始的javascript:。但是,://deals.razerzone.com/在进一步出现://.

let rurl = "javascript://deals.razerzone.com/";
rurl.split("://");
//=> [ 'javascript', 'deals.razerzone.com/' ]
rurl.split("://")[1];
//=> 'deals.razerzone.com/'
rurl.split("://")[1].split("/");
//=> [ 'deals.razerzone.com', '' ]
rurl.split("://")[1].split("/")[0];
("deals.razerzone.com");

正如下面这段代码中突出显示的语法所给出的那样,//它被视为单行注释,因此注释掉deals.razerzone.com/了有效负载的一部分。

javascript://deals.razerzone.com/

接下来,我们需要一种方法来打破单行注释并附加 JavaScript 代码。我从Gareth Heyes那里学到了一个技巧:JavaScriptU+2028 Line Separator字符视为换行符,从而产生换行符1

javascript://deals.razerzone.com/%E2%80%A8alert(document.domain)

话虽如此,任何形式的行终止符都可以在这里使用,包括换行符%0A) 和回车符%0D)。我喜欢这个U+2028技巧,因为我遇到了换行符被剥离的情况,我需要使用U+2028.

最后,绕过indexOf()签入(1.),可以附加https://deals.razerzone.com到有效负载的末尾并将其注释掉,以免影响alert()调用。

javascript://deals.razerzone.com/%E2%80%A8alert(document.domain)//https://deals.razerzone.com
绕过Razer基于DOM的XSS补丁可以教给我们什么

if这说明了可以绕过该语句的多种方式之一。一些简单的东西javascript:alert()//https://deals.razerzone.com也会起作用。我发现的一个更简单、更幽默的绕过方法是javascript:alert("https://deals.razerzone.com/"). 你能确定为什么这会奏效吗?

对易受攻击的代码的快速修复是验证rurl.indexOf(siteURL) == 0和硬编码(siteURL注意https://deals.razerzone.com/附加的/)。这将确保rurl从 开始https://deals.razerzone.com/,防止重定向到外部主机并减轻基于 DOM 的 XSS 漏洞。

但是,这种快速修复并不能解决让未来读者感到困惑的问题。此外,代码非常脆弱,而且无法保证未来的发展。我们在很大程度上依赖/siteURL. 删除最终版本/siteURL整个修复程序就会分崩离析。这种方法感觉更像是一种“黑客”

使用URLAPI 的更充分的修复可能是:

if (razerUserLogin) {
  let params = new URL(document.location).searchParams;
  let rurl = params.get("rurl");
  rurl = new URL(rurl);
  // Validate redirect URI to ensure user is redirected to trusted
  // deals.razerzone.com endpoint. This prevents unvalidated redirects
  // to malicious pages and DOM-based XSS using the javascript: protocol.
  // Reference: https://hackerone.com/reports/292200
  if (rurl.hostname == "deals.razerzone.com" && rurl.protocol == "https:") {
    location.href = rurl;
  }
}

该解决方案解释了为什么需要该if语句,引用了引发代码更改的 HackerOne 报告,并且在嵌套split()方法的深处没有任何惊喜。将来查看此代码的开发人员不必逐步执行split()(2.) 中所述的方法调用来弄清楚幕后发生了什么。

此外,@filedescriptor指出,此实现还解决了 Razer 初始化rurllocation.hash. 使用 Razer 的方法从 URI解析rurl可能会导致处理 URI 片段(即/#rurl=)的困难——这种方法允许攻击者从防火墙规则和服务器日志中隐藏 XSS 有效负载。2

更精细的解决方案可能是设置location.href'https://deals.razerzone.com/' + rurl, where rurl = new URL(rurl).pathname。然后,无论通过rurlGET 参数提供什么,客户端总是会重定向到位于deals.razerzone.com. 这将使我们不必编写任何验证。

结论

这篇博文的部分目的是说明我如何结合设计补丁来更好地理解我正在利用的代码。通常,发布的公告和漏洞报告完全关注漏洞利用,而很少关注缓解策略。对于漏洞披露的新手:我希望这篇博文能够展示我的“学会做事;然后打破它”的安全研究方法。查看大量 JavaScript 代码以及我对 API 的熟悉程度URL使我能够更轻松地识别 Razer 补丁的问题。

此外,我发现在提交漏洞报告时向供应商建议补丁是成功的。在向漏洞赏金计划报告时,包括具体的缓解措施可以减少解决问题的时间和支付时间。我发现供应商通常更容易接受,因为补丁封装了一种替代方法,可以替代受影响产品的当前实现。这是我经常在我的研讨会上建议学生做的事情。

最后,无论您投入多少时间重构代码以解决安全漏洞,最简单的解决方案最终总会浮出水面。

❯ curl https://deals.razerzone.com/
curl: (6) Could not resolve host: deals.razerzone.com

  1. 在 JavaScript 中处理单行注释中的行终止符的代码可以在 Google Chrome 的 JavaScript 引擎V8中看到。解析器不会将行终止符视为单行注释的一部分——遵守 ECMAScript 规范。 ↩︎
  2. URI 片段部分永远不会发送到应用程序服务器。 ↩︎

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注