Link Search Menu Expand Document

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