CVE-2020-7245 vulnerability analysis

CVE-2020-7245 vulnerability analysis

Introduction

The vulnerability is a CTFd account takeover vulnerability. There is a logic vulnerability in the registration and modification of the password, which leads to the modification of any account password.

Affected version: v2.0.0-2.2.2

Vulnerability analysis

First locate the user registry: /CTFd/auto.py

@auth.route("/register", methods=["POST", "GET"])
@check_registration_visibility
@ratelimit(method="POST", limit=10, interval=5)
def register():
    errors = get_errors()
    if request.method == "POST":
        name = request.form["name"]
        email_address = request.form["email"]
        password = request.form["password"]

        name_len = len(name) == 0
        names = Users.query.add_columns("name", "id").filter_by(name=name).first()
        emails = (
            Users.query.add_columns("email", "id")
            .filter_by(email=email_address)
            .first()
        )
        pass_short = len(password.strip()) == 0
        pass_long = len(password) > 128
        valid_email = validators.validate_email(request.form["email"])
        team_name_email_check = validators.validate_email(name)

        if not valid_email:
            errors.append("Please enter a valid email address")
        if email.check_email_is_whitelisted(email_address) is False:
            errors.append(
                "Only email addresses under {domains} may register".format(
                    domains=get_config("domain_whitelist")
                )
            )
        if names:
            errors.append("That user name is already taken")
        if team_name_email_check is True:
            errors.append("Your user name cannot be an email address")
        if emails:
            errors.append("That email has already been used")
        if pass_short:
            errors.append("Pick a longer password")
        if pass_long:
            errors.append("Pick a shorter password")
        if name_len:
            errors.append("Pick a longer user name")

        if len(errors) > 0:
            return render_template(
                "register.html",
                errors=errors,
                name=request.form["name"],
                email=request.form["email"],
                password=request.form["password"],
            )
        else:
            with app.app_context():
                user = Users(
                    name=name.strip(),
                    email=email_address.lower(),
                    password=password.strip(),
                )
                db.session.add(user)
                db.session.commit()
                db.session.flush()

                login_user(user)

                if config.can_send_mail() and get_config(
                    "verify_emails"
                ):  # Confirming users is enabled and we can send email.
                    log(
                        "registrations",
                        format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}",
                    )
                    email.verify_email_address(user.email)
                    db.session.close()
                    return redirect(url_for("auth.confirm"))
                else:  # Don't care about confirming users
                    if (
                        config.can_send_mail()
                    ):  # We want to notify the user that they have registered.
                        email.sendmail(
                            request.form["email"],
                            "You've successfully registered for {}".format(
                                get_config("ctf_name")
                            ),
                        )

        log("registrations", "[{date}] {ip} - {name} registered with {email}")
        db.session.close()

        if is_teams_mode():
            return redirect(url_for("teams.private"))

        return redirect(url_for("challenges.listing"))
    else:
        return render_template("register.html", errors=errors)

Most of the above code is input detection, and the key parts are extracted:

def register():
    errors = get_errors()
    if request.method == "POST":
        name = request.form["name"]
        email_address = request.form["email"]
        password = request.form["password"]

        name_len = len(name) == 0
        names = Users.query.add_columns("name", "id").filter_by(name=name).first()
        emails = (
            Users.query.add_columns("email", "id")
            .filter_by(email=email_address)
            .first()
        )
        pass_short = len(password.strip()) == 0
        pass_long = len(password) > 128
        valid_email = validators.validate_email(request.form["email"])
        team_name_email_check = validators.validate_email(name)
        
		if len(errors) > 0:			#检测出错
        
        '''注册账户密码插入数据库'''
   		else:					
            with app.app_context():
                user = Users(
                    name=name.strip(),
                    email=email_address.lower(),
                    password=password.strip(),
                )
                db.session.add(user)
                db.session.commit()
                db.session.flush()

                login_user(user)

                if config.can_send_mail() and get_config(
                    "verify_emails"
                ):  # Confirming users is enabled and we can send email.
                    log(
                        "registrations",
                        format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}",
                    )
                    email.verify_email_address(user.email)
                    db.session.close()
                    return redirect(url_for("auth.confirm"))

The upper part of the upper part accepts user input:

def register():
    errors = get_errors()
    if request.method == "POST":
        name = request.form["name"]
        email_address = request.form["email"]
        password = request.form["password"]

        name_len = len(name) == 0
        names = Users.query.add_columns("name", "id").filter_by(name=name).first()
        emails = (
            Users.query.add_columns("email", "id")
            .filter_by(email=email_address)
            .first()
        )
        pass_short = len(password.strip()) == 0
        pass_long = len(password) > 128
        valid_email = validators.validate_email(request.form["email"])
        team_name_email_check = validators.validate_email(name)

The key is here:

names = Users.query.add_columns("name", "id").filter_by(name=name).first()

When judging whether the user has been registered, the name is used directly, that is, the account name entered by the user, and there is no filtering.

In the second half, when the registration is successful, insert the account, password, and email into the database:

with app.app_context():
                user = Users(
                    name=name.strip(),
                    email=email_address.lower(),
                    password=password.strip(),
                )
                db.session.add(user)
                db.session.commit()
                db.session.flush()

But here, the operation of removing spaces is performed on the account entered by the user. (That is to say, if the account m1sn0w exists in the database, but if the account name I entered when registering: space m1sn0w, then the account will not be prompted when registering, but the user name m1sn0w will be inserted into the database In, that is, there is a user with the same name in the database)

Next is the second point of use (change password): extract the main code

@auth.route("/reset_password", methods=["POST", "GET"])
@auth.route("/reset_password/<data>", methods=["POST", "GET"])
@ratelimit(method="POST", limit=10, interval=60)
def reset_password(data=None):
    if data is not None:
        try:
            name = unserialize(data, max_age=1800)
        except (BadTimeSignature, SignatureExpired):
            return render_template(
                "reset_password.html", errors=["Your link has expired"]
            )
        except (BadSignature, TypeError, base64.binascii.Error):
            return render_template(
                "reset_password.html", errors=["Your reset token is invalid"]
            )

        if request.method == "GET":
            return render_template("reset_password.html", mode="set")
        if request.method == "POST":
            user = Users.query.filter_by(name=name).first_or_404()
            user.password = request.form["password"].strip()
            db.session.commit()
            log(
                "logins",
                format="[{date}] {ip} -  successful password reset for {name}",
                name=name,
            )
            db.session.close()
            return redirect(url_for("auth.login"))

We know that when changing the password, an email will be sent to the corresponding mailbox, and the password can be changed after clicking. (The data value above is a string of values ​​after the URL sent to the specified mailbox)

Next, see what the data value is: /CTFd/utils/email/__init__.py

def forgot_password(email, team_name):
    token = serialize(team_name)
    text = """Did you initiate a password reset? Click the following link to reset your password:

{0}/{1}

""".format(
        url_for("auth.reset_password", _external=True), token
    )

    return sendmail(email, text)

As you can see, after serializing the user name, it is spliced ​​to the corresponding URL and sent to the mailbox. (Through the previous analysis, we know that there are two accounts in the database with the same name, so when the password is changed, the password of the first user will be changed)

(Some articles say that the current user needs to be changed to another user name, but it does not seem to be needed)

if request.method == "POST":
            user = Users.query.filter_by(name=name).first_or_404()
            user.password = request.form["password"].strip()
            db.session.commit()

The user it takes out here is the first user (that is, the user registered previously)

Therefore, the approximate utilization method is as follows:

1. Register an account with the same name as the user name you want to modify, but add a space when registering

2. Click to modify the password, confirm in the mailbox, and then modify the specified user password

Guess you like

Origin blog.csdn.net/gental_z/article/details/108513592