saarCTF 2022 – bytewarden
bytewarden is a Python/Django web-based password manager that was part of saarCTF 2022.
Overview
Flags were stored as normal password entries. The server allowed registrations, and allowed recovery of forgotten passwords by way of answering a secret question. These secret questions could be set to be shared by multiple users.
Two-factor authentication is automatically enabled upon registration, and is not supposed to be bypassed by resetting the account password via the secret question/answer combination.
Passwords, as well as the secret question and answers listed on the shared questions page, were xor’d using JavaScript. The code for this “encryption” can be found here.
Vulnerability
All flags were stored under a user account that had its secret question stored as a shared question, so both the question and answer could be acquired by any registered user. Thus, the basic premise of the exploit was simple:
- Register a new user
- Use this new access to gain access to all shared questions
- Recover the flag user using this newfound knowledge
- Read the password from the flag user’s password store
Execution
The flow described above was slightly complicated by two issues:
- As mentioned in the overview, all “sensitive” data was “encrypted”, so the exploit would have to implement the same crypto
- Recovering the account would not allow access to the password store without 2fa
Issue one was easily solved with the following python snippet, as the crypto used was completely open source, and happened completely on the client.
def xor_dec_data(data, key_in):
key = [ord(k) for k in key_in]
out = []
for i in range(len(data)):
out.append(chr(data[i] ^ key[i % len(key)]))
return ''.join(out)
def xor_enc_data(data, key_in):
key = [ord(k) for k in key_in]
data = [ord(d) for d in data]
out = []
for i in range(len(data)):
out.append(chr(data[i] ^ key[i % len(key)]))
return b64encode(''.join(out).encode())
Bypassing the 2fa requirement was also simpler than expected: the 2fa check was only on the main vault page, directly going to /vault/passwords
completely bypassed this and allowed access to all passwords.
Keeping these issues, and their solutions, in mind, the complete flow was now as follows:
- Send a GET request to
/users/signup
to get a CSRF token - Register an account by POSTing the credentials to
/users/signup/
- Login to this account via
/users/login/
- Parse the shared question and answer for the current flag account from
/questions/shared/
, decrypt them, and store them
You now have the secret question, and answer, for the current flag user.
In a new session:
- Send a GET request to
/users/recover/
to get a CSRF token - Using the question and answer acquired in step 4, recover the flag user’s account by POSTing them to
/users/recover/
- Grab the flag from the user’s password vault by GETing
/vault/passwords/
, parsing the response, and decrypting the password.
Implementing code for this flow is left as an exercise for the reader.