## # 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://docs.github.com/en/authentication/connecting-to-github-with-ssh/testing-your-ssh-connection']], 'Notes' => { 'Reliability' => UNKNOWN_RELIABILITY, 'Stability' => UNKNOWN_STABILITY, 'SideEffects' => UNKNOWN_SIDE_EFFECTS } ) ) 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 recursively 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