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:

  1. Register a new user
  2. Use this new access to gain access to all shared questions
  3. Recover the flag user using this newfound knowledge
  4. Read the password from the flag user’s password store

Execution

The flow described above was slightly complicated by two issues:

  1. As mentioned in the overview, all “sensitive” data was “encrypted”, so the exploit would have to implement the same crypto
  2. 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:

  1. Send a GET request to /users/signup to get a CSRF token
  2. Register an account by POSTing the credentials to /users/signup/
  3. Login to this account via /users/login/
  4. 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:

  1. Send a GET request to /users/recover/ to get a CSRF token
  2. Using the question and answer acquired in step 4, recover the flag user’s account by POSTing them to /users/recover/
  3. 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.