In the security release of 11th Jan 2024, Gitlab patched a critical vulnerability “Account Takeover via Password Reset without user interactions”, for which CVE 2023-7028 was assigned.
This looked interesting, so we decided to look into the issue and the patch. This was our first time setting up the env and looking into the gitlab codebase.
While analyzing this CVE, we also discovered another issue that could potentially result in “Account Takeover on GitLab” based on certain criteria which is explained after the CVE analysis.
Below is the POC of CVE 2023-7028 that was used to take over any user account.

If you observe the screenshot, you’ll see that the request is using content-type application/x-www-form-urlencoded, and within request, there’s a parameter called user which contains an email address. Researcher added an extra [] which changed the email key from string type to list that contains multiple email addresses. So instead of just having the user’s email like this:

The attacker changed it to:

Let’s review the code to know how the above POC lead to account takeover of any user.
-
ActionController internal module is responsible for handling the controller request. It calls the method
createof “password_controller.rb”
-
In the
createmethod,send_reset_password_instructionsmethod is getting called with post request body included as one of the fields in the “resource_params” parameter. Observe the debug console for the emails passed in the HTTP request body.
-
In the
send_reset_password_instructionsmethod,attributes.deletemethod extracts theemailkey from theattributesobject passed as an argument and returns the list of emails supplied as shown below.by_email_with_errorsmethod is called with the extracted email.
-
In this method,
by_email_with_errorsmethod is being called with the extracted emails.
-
In this method, it is again passing the emails to
find_by_any_emailmethod with a default parameter valueconfirmedset totrue
-
In this method,
by_any_emailmethod is getting called.
-
by_user_emailmethod fetches the user records from the database if any user exists with the provided email address.

-
In the same manner,
by_emailsmethod fetched the user records based on theemailstable with the join onuserstable in the database.

-
Now, the
user_id_for_emailsmethod fetches the user based on the private commit email(We will explain this later in detail)
-
itemslist will contain the user records from the methods:by_any_email,by_emails,user_id_for_emails.

-
Now the first record is fetched and will be used to generate the reset token

-
The reset password token of the user will be sent to all the emails supplied

Possible Account Takeover of any user By Using Private Commit Email
Video POC
We observed the below code snippet.
https://gitlab.com/gitlab-org/gitlab/-/blob/d8c1bdad1cccaf9cf8f6c62af49907135b907b0e/app/models/user.rb#L750-763
|
|
In this code, the PrivateCommitEmail method is called, which extracts the user ID from the private commit email and returns the user based on the extracted user ID. After extracting the user, it creates a password reset token for the extracted user and sends it to the private commit email instead of sending it to the user’s registered email.
Attack Scenarios
Scenario 1:
An attacker can takeover any other user account based on the user ID if they have obtained the mail in the private commit email format(<victimid>-<anything>@custom_hostname). In this case, attacker does not have to register the account on gitlab.
Scenario 2:
An attacker can register using the private commit email format (<victimid>-<anything>@custom_hostname) of any user. Subsequently, during the password reset process, the attacker will receive the reset password of the victim user.
By default, custom name is set to users.noreply.<default_host>. In this case, the attacker should have access to <victimid><anything>@users.noreply.<default_host>

If the admin has set a custom host, the attacker should then have access to <victimid><anything>@<custom_host>. Here, the custom_host is set to “example.com”

There is a logic flaw in Scenario 2 in the password reset email functionality. Below is an explanation for the same:
-
Observe that
by_user_emailandby_emailsmethods are called to fetch the user details based on the users and emails table from the database.
-
The list
itemscontains the attacker’s user records. After this,PrivateCommitEmails.user_id_for_emailsis called, which fetches the record based on the victim ID supplied in the email, and there is a hostname check based on the below method.
|
|
-
Now, the list
itemscontains the victim’s User record as well.
-
Now, the union query is executed. Observe below that the list
itemsand the return list from the query are in the same order, which is an ideal scenario. However, sometimes the listitemsand the return list from the query are in a different order.

-
First record is fetched which is the victim user and will be used to generate the reset token

-
After this, the reset password token of the victim user is sent to the attacker’s email

CVE 2023-7028 Patch:
We observed that the above attack is not working in the latest version due to the patch introduced for CVE 2023-7028.
Below is the code snippet that does not call the by_email_with_errors method, which has the support for private commit emails.
Email record is fetched from the emails table if the email exists in the database and then the user record is fetched based on the join on the users table.
The supplied email is being converted into a string, so even adding support for multiple email addresses by using [] won’t work.
|
|
We reported this bug to gitlab on hackerone. It was marked as informative by gitlab since the patch for the CVE 2023-7028 also resolved this issue.
References: