rapid7/metasploit-framework

View on GitHub
docs/metasploit-framework.wiki/How-to-use-the-Git-mixin-to-write-an-exploit-module.md

Summary

Maintainability
Test Coverage
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][1] 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][3]),
so to host a malicious repository with Metasploit, the exploit module needs to
leverage the `Msf::Exploit::Remote::HttpServer` mixin. Additionally,
the [Git][1] and [Git SmartHttp][2] 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:

```ruby
  # 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:

```ruby
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.:

```ruby
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.

```ruby
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:

```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::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

```msf
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
```

[1]: https://github.com/rapid7/metasploit-framework/blob/b1a6d9d30778bed11276ac8685f88d0a4dc98e19/lib/msf/core/exploit/git.rb
[2]: https://github.com/rapid7/metasploit-framework/blob/b1a6d9d30778bed11276ac8685f88d0a4dc98e19/lib/msf/core/exploit/git/smart_http.rb
[3]: https://git-scm.com/docs/http-protocol