This page walks through the process of creating an exploit module for vulnerable Git clients.
Building a Repository
Many of the existing Git exploits in Metasploit rely on being able to host a valid repository that a Git client can successfully clone. So to get started with building an exploit, the contents of the repo need to be decided on first.
Let’s say that the repository is something like the following:
space@vm:~/test-repo$ ls -al
total 20
drwxrwxr-x 4 space space 4096 Sep 16 14:06 .
drwxr-x--- 23 space space 4096 Sep 16 14:05 ..
drwxrwxr-x 2 space space 4096 Sep 16 14:06 dir
-rw-rw-r-- 1 space space 10 Sep 16 14:06 file.txt
drwxrwxr-x 7 space space 4096 Sep 16 14:06 .git
space@vm:~/test-repo$ ls -al dir
total 12
drwxrwxr-x 2 space space 4096 Sep 16 14:06 .
drwxrwxr-x 4 space space 4096 Sep 16 14:06 ..
-rw-rw-r-- 1 space space 5 Sep 16 14:06 test_file.txt
The .git
directory is the only component of the repository that won’t be sent, so the repository will consist of the file.txt
, the dir
folder, and the test_file.txt
file that lives within the dir
folder. Every file and directory inside the repo is represented as a Git object: File contents are represented as blob objects which get coupled together to form a tree object. Lastly, a commit object is created to hold information about the tree object, including the tree’s sha, the author of the commit, a commit message, etc.
There will need to be two tree objects to represent the contents of dir
and the contents of the root of the repository. Starting with the contents of dir
, a blob object needs to be created to represent the contents of test_file.txt
:
space@vm:~/test-repo$ cat dir/test_file.txt
test
The Git mixin contains the functionality for building a Git object. To build a blob object, the build_blob_object()
class method should be used:
>> contents = "test\n"
=> "test\n"
>> blob = Msf::Exploit::Git::GitObject.build_blob_object(contents)
=>
#<Msf::Exploit::Git::GitObject:0x00007fe163c75cd0
The resulting object will contain the object type, its original contents, its compressed contents, its sha, and its path (where the commit object will be stored client side). Since this will be the only file in the dir
folder, the tree object can be created with Msf::Exploit::Git::GitObject.build_tree_object()
. A tree object is represented differently, holding information about each file contained in the directory, such as file permissions, file name, object type, and the file’s sha1 hash. Because of that, the build_tree_object()
expects a hash or an array of hashes, where each hash looks like the following:
>> tree_entry =
{
mode: '100644',
file_name: 'test_file.txt',
sha1: blob.sha1
}
And using that, the tree object can now be created:
>> tree_object = Msf::Exploit::Git::GitObject.build_tree_object(tree_entry)
=>
#<Msf::Exploit::Git::GitObject:0x00007fe161b0cd78
Now that the dir
folder is represented in Git objects, we can represent the root of the repository. That just requires creating a blob
object for file.txt
, creating a tree
object representing the top-level directory, and finally a commit object.
Again, a blob object needs to be created to represent the contents of the remaining file:
space@vm:~/test-repo$ cat file.txt
some text
>> contents = "some text\n"
=> "some text\n"
>> file_blob = Msf::Exploit::Git::GitObject.build_blob_object(contents)
=>
#<Msf::Exploit::Git::GitObject:0x00007fe163bf54b8
...
Then, a new tree object needs to be created to represent the top-level directory, which includes file.txt
and the dir
folder:
?> entries = [
?> {
?> mode: '100644',
?> file_name: 'file.txt',
?> sha1: file_blob.sha1
?> },
?> {
?> mode: '040000',
?> file_name: 'dir',
?> sha1: tree_object.sha1
?> }
>> ]
=> [{:mode=>"100644", :file_name=>"file.txt", :sha1=>"b649a9bf89116c581f8329b8ec3c79a86a70...
>> top_level_obj = Msf::Exploit::Git::GitObject.build_tree_object(entries)
The build_commit_object()
method takes a hash that expects the sha1 hash for the tree created, the sha1 hash for the parent commit if one exists, and optional data such as an author name, email address, company name, commit message, etc. If the user chooses not to pass in data for the optional data, Faker
will generate random data for them.
>> commit_object = Msf::Exploit::Git::GitObject.build_commit_object(tree_sha1: top_level_obj.sh
a1)
=>
#<Msf::Exploit::Git::GitObject:0x00007fe1533ac848
...
>> commit_object
=>
#<Msf::Exploit::Git::GitObject:0x00007fe1533ac848
@compressed=
"x\x9C\x95\xCEA\x0E\xC2 \x10\x05P\xD7\x9Cb<@\r\x1DZ\xCA\xC2\x18\xE3\xCE\xA8g0XF!\xB6\xD0\x00]x{I\xED\x05\\\xCD\xE4'\xF3\xFE\xF4a\x1C]\x06\x14j\x93#\x11pe\b\el5u]cL#\xD1\x18\xC9\x05\x97\x92\x04*\xF3h\xA5P}\xC7\x89\xE99\xDB\x10\xE1\xEA\x92\xF6&j\xB8\xCC\x93\xD5\x03\xEC\xDF\xCB\xBC\x0Fk~\xB43\ri\xE7)\x1F\xA0\xAEU[\x10l\x05T\x85\xE4\xAC_\xCA3\xFD\xC7\xA8\x0E%\nQ\xE3\xAA\xB0\xB3w\xD9\x95\xA3\x1F\a9@\x98\xC8\xC3\xAB\xEC\x91\xA6\x90\\\x0E\xF1\x03\xCF\xF2\xED\xC9\xF9T\xDD\x82\x8D[\xF6\x05s\xF7P\x89",
@content=
"tree 08de2425ae774dd462dd603066e328db5638c70e\nauthor Lisandra Kuphal <kuphal_lisandra@huels.net> 1185328253 -0300\ncommitter Lisandra Kuphal <kuphal_lisandra@huels.net> 872623312 -0300\n\nInitial commit to open git repository for Bins-Mohr!\n",
@path="01/8856fe17403b0991e5d1d3eb7f62dca4d8e951",
@sha1="018856fe17403b0991e5d1d3eb7f62dca4d8e951",
@type="commit">
That’s all that is needed to create a valid repository in Metasploit.
Hosting the Repository
Metasploit’s current implementation of the Git protocol works over HTTP (SmartHttp docs), so to host a malicious repository with Metasploit, the exploit module needs to leverage the Msf::Exploit::Remote::HttpServer
mixin. Additionally, the Git and Git SmartHttp mixins need to be included to build objects and create appropriate responses for the client’s requests.
The module should look similar to other exploit modules that use the HttpServer mixin, defining an on_request_uri()
method, a primer()
method, and an exploit()
method. The primer()
method is first to execute, so setup for things like the repository uri can happen there:
# Creates a random uri for the Git repo, ensuring that there are no spaces
def create_git_uri
"/#{Faker::App.name.downcase}.git".gsub(' ', '-')
end
# Uses GIT_URI datastore option or randomly generates a repo URI
# Registers the URI with the http server and prints the entire path that client should pass to git clone
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)
end
Next, the exploit()
method can be used to set up the repository. The code used in the Building a Repository
section can be placed here before entering the listen / accept loop.
The on_request_uri()
method is where most of the module logic will live. No matter what the client sends, the request should first be parsed by Msf::Exploit::Git::SmartHttp::Request.parse_raw_request()
. The parse_raw_request()
method will format the request so it is easier to work with. The first request that a client will send when cloning a repository is a reference discovery request. The client will expect things like server capabilities and the reference that HEAD
points to in the response. Since this is a simple repo only one branch will exist, so HEAD
will point to refs/heads/master
and refs/heads/master
will point to the latest commit in the repo, which in this case is the only commit in the repo. This can be represented as the following hash:
refs =
{
'HEAD' => 'refs/heads/master',
'refs/heads/master' => commit_object.sha1
}
Creating a proper response to a ref-discovery
request is done through Msf::Exploit::Git::SmartHttp.get_ref_discovery_response()
. It takes two parameters: The request object from parse_raw_request()
and the above refs
hash. After the response is built, it can be sent back to the client.:
response = get_ref_discovery_response(request, @refs)
cli.send_response(response)
If the client successfully receives the ref-discovery
response, it will then send an upload-pack
request. The upload-pack
request is a POST
request containing the client’s capabilities and a ‘want’ list for objects in the repository. To create a proper response, the Msf::Exploit::Git::SmartHttp.get_upload_pack_response()
method should be used. Again, this method accepts two arguments. The first is the parsed request from the client, and the second is an array of all objects that exist in the repo. The get_upload_pack_response()
method will check the sha1 hash of each object against the hashes in the want list that the client sent and send only the requested object hashes.
response = get_upload_pack_response(request, @git_objs)
cli.send_response(response)
Upon receiving the upload-pack
response from the server, the client will build out the repository.
Putting it all together, the module should look something like the following:
##
# 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::Remote::HttpServer
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Git Clone Test',
'Description' => %q{
},
'License' => MSF_LICENSE,
'Author' => [ ],
'References' => [ ],
'DisclosureDate' => '2022-09-22',
'Platform' => [ 'unix' ],
'Arch' => ARCH_CMD,
'Targets' => [
[ 'Automatic Target', {}]
],
'DefaultTarget' => 0,
'Notes' => {}
)
)
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
# create blob object for contents of 'test_file.txt'
contents = "test\n"
blob = Msf::Exploit::Git::GitObject.build_blob_object(contents)
# create tree object representing 'test_file.txt' in 'dir' folder
tree_entry =
{
mode: '100644',
file_name: 'test_file.txt',
sha1: blob.sha1
}
tree_object = Msf::Exploit::Git::GitObject.build_tree_object(tree_entry)
# create blob object for contents of 'file.txt'
contents = "some text\n"
file_blob = Msf::Exploit::Git::GitObject.build_blob_object(contents)
# create tree object representing top-level directory of repo
entries =
[
{
mode: '100644',
file_name: 'file.txt',
sha1: file_blob.sha1
},
{
mode: '040000',
file_name: 'dir',
sha1: tree_object.sha1
}
]
top_level_obj = Msf::Exploit::Git::GitObject.build_tree_object(entries)
# create commit
commit_object = Msf::Exploit::Git::GitObject.build_commit_object(tree_sha1: top_level_obj.sha1)
# create list of objects in repository, as the
# client will request them to build the repository
@git_objs =
[
commit_object, top_level_obj, tree_object,
file_blob, tree_object, blob
]
@refs =
{
'HEAD' => 'refs/heads/master',
'refs/heads/master' => commit_object.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)
end
def on_request_uri(cli, req)
request = Msf::Exploit::Git::SmartHttp::Request.parse_raw_request(req)
case request.type
when 'ref-discovery'
response = get_ref_discovery_response(request, @refs)
fail_with(Failure::UnexpectedReply, 'Git client did not send a valid ref-discovery request') unless response
when 'upload-pack'
response = get_upload_pack_response(request, @git_objs)
fail_with(Failure::UnexpectedReply, 'Git client did not send a valid upload-pack request') unless response
else
fail_with(Failure::UnexpectedReply, 'Git client did not send a valid request')
end
cli.send_response(response)
end
end
Running the module
The module will start the http server and print the repo to clone
msf6 > use exploit/multi/http/git_clone_test
[*] No payload configured, defaulting to cmd/unix/python/meterpreter/reverse_tcp
msf6 exploit(multi/http/git_clone_test) > set srvport 9999
srvport => 9999
msf6 exploit(multi/http/git_clone_test) > set lhost 192.168.140.1
lhost => 192.168.140.1
msf6 exploit(multi/http/git_clone_test) > set srvhost 192.168.140.1
srvhost => 192.168.140.1
msf6 exploit(multi/http/git_clone_test) > run
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.
msf6 exploit(multi/http/git_clone_test) > [*] Started reverse TCP handler on 192.168.140.1:4444
[*] Using URL: http://192.168.140.1:9999/MOYuJfC
[*] Server started.
[*] Git repository to clone: http://192.168.140.1:9999/y-find.git
Once the repository is cloned, you should expect to see the same contents as the test-repo
at the beginning of this document:
space@ubuntu:~$ git clone http://192.168.140.1:9999/y-find.git
Cloning into 'y-find'...
remote: Enumerating objects: 6, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), 401 bytes | 200.00 KiB/s, done.
space@ubuntu:~$ cd y-find
space@ubuntu:~/y-find$ ls -al
total 20
drwxrwxr-x 4 space space 4096 Sep 22 12:05 .
drwxr-x--- 22 space space 4096 Sep 22 12:05 ..
drwxrwxr-x 2 space space 4096 Sep 22 12:05 dir
-rw-rw-r-- 1 space space 10 Sep 22 12:05 file.txt
drwxrwxr-x 8 space space 4096 Sep 22 12:05 .git
space@ubuntu:~/y-find$ cat dir/test_file.txt
test
space@ubuntu:~/y-find$ cat file.txt
some text