-
Notifications
You must be signed in to change notification settings - Fork 14.6k
Combine ssh_login and ssh_login_pubkey modules #20704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dwelch-r7
wants to merge
1
commit into
rapid7:master
Choose a base branch
from
dwelch-r7:combine-ssh-login-modules
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+274
−340
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| module Metasploit::Framework | ||
| class KeyCollection < Metasploit::Framework::CredentialCollection | ||
| attr_accessor :key_data | ||
| attr_accessor :key_path | ||
| attr_accessor :private_key | ||
| attr_accessor :error_list | ||
| attr_accessor :ssh_keyfile_b64 | ||
|
|
||
| # Override CredentialCollection#has_privates? | ||
| def has_privates? | ||
| @key_data.present? | ||
| end | ||
|
|
||
| def realm | ||
| nil | ||
| end | ||
|
|
||
| def valid? | ||
| @error_list = [] | ||
| @key_data = Set.new | ||
|
|
||
| if @private_key.present? | ||
| results = validate_private_key(@private_key) | ||
| elsif @key_path.present? | ||
| results = validate_key_path(@key_path) | ||
| else | ||
| @error_list << 'No key path or key provided' | ||
| raise RuntimeError, 'No key path or key provided' | ||
| end | ||
|
|
||
| if results[:key_data].present? | ||
| @key_data.merge(results[:key_data]) | ||
| else | ||
| @error_list.concat(results[:error_list]) if results[:error_list].present? | ||
| end | ||
|
|
||
| @key_data.present? | ||
| end | ||
|
|
||
| def validate_private_key(private_key) | ||
| key_data = Set.new | ||
| error_list = [] | ||
| begin | ||
| if Net::SSH::KeyFactory.load_data_private_key(private_key, @password, false).present? | ||
| key_data << private_key | ||
| end | ||
| rescue StandardError => e | ||
| error_list << "Error validating private key: #{e}" | ||
| end | ||
| {key_data: key_data, error_list: error_list} | ||
| end | ||
|
|
||
| def validate_key_path(key_path) | ||
| key_data = Set.new | ||
| error_list = [] | ||
|
|
||
| if File.file?(key_path) | ||
| key_files = [key_path] | ||
| elsif File.directory?(key_path) | ||
| key_files = Dir.entries(key_path).reject { |f| f =~ /^\x2e|\x2epub$/ }.map { |f| File.join(key_path, f) } | ||
| else | ||
| return {key_data: nil, error: "#{key_path} Invalid key path"} | ||
| end | ||
|
|
||
| key_files.each do |f| | ||
| begin | ||
| if read_key(f).present? | ||
| key_data << File.read(f) | ||
| end | ||
| rescue StandardError => e | ||
| error_list << "#{f}: #{e}" | ||
| end | ||
| end | ||
| {key_data: key_data, error_list: error_list} | ||
| end | ||
|
|
||
|
|
||
| def each | ||
| prepended_creds.each { |c| yield c } | ||
|
|
||
| if @user_file.present? | ||
| File.open(@user_file, 'rb') do |user_fd| | ||
| user_fd.each_line do |user_from_file| | ||
| user_from_file.chomp! | ||
| each_key do |key_data| | ||
| yield Metasploit::Framework::Credential.new(public: user_from_file, private: key_data, realm: realm, private_type: :ssh_key) | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| if @username.present? | ||
| each_key do |key_data| | ||
| yield Metasploit::Framework::Credential.new(public: @username, private: key_data, realm: realm, private_type: :ssh_key) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def each_key | ||
| @key_data.each do |data| | ||
| yield data | ||
| end | ||
| end | ||
|
|
||
| def read_key(file_path) | ||
| @cache ||= {} | ||
| @cache[file_path] ||= Net::SSH::KeyFactory.load_private_key(file_path, password, false) | ||
| @cache[file_path] | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| require 'net/ssh/command_stream' | ||
| require 'metasploit/framework/login_scanner/ssh' | ||
| require 'metasploit/framework/credential_collection' | ||
| require 'metasploit/framework/key_collection' | ||
|
|
||
| class MetasploitModule < Msf::Auxiliary | ||
| include Msf::Auxiliary::AuthBrute | ||
|
|
@@ -16,6 +17,8 @@ class MetasploitModule < Msf::Auxiliary | |
| include Msf::Exploit::Remote::SSH::Options | ||
| include Msf::Sessions::CreateSessionOptions | ||
| include Msf::Auxiliary::ReportSummary | ||
| include Msf::Exploit::Deprecated | ||
| moved_from 'auxiliary/scanner/ssh/ssh_login_pubkey' | ||
|
|
||
| def initialize | ||
| super( | ||
|
|
@@ -26,7 +29,8 @@ def initialize | |
| and connected to a database this module will record successful | ||
| logins and hosts so you can track your access. | ||
| }, | ||
| 'Author' => ['todb'], | ||
| 'Author' => ['todb', 'RageLtMan'], | ||
| 'AKA' => ['ssh_login_pubkey'], | ||
| 'References' => [ | ||
| [ 'CVE', '1999-0502'], # Weak password | ||
| [ 'ATT&CK', Mitre::Attack::Technique::T1021_004_SSH ] | ||
|
|
@@ -37,7 +41,10 @@ def initialize | |
|
|
||
| register_options( | ||
| [ | ||
| Opt::RPORT(22) | ||
| Opt::RPORT(22), | ||
| OptPath.new('KEY_PATH', [false, 'Filename or directory of cleartext private keys. Filenames beginning with a dot, or ending in ".pub" will be skipped. Duplicate private keys will be ignored.']), | ||
| OptString.new('KEY_PASS', [false, 'Passphrase for SSH private key(s)']), | ||
| OptString.new('PRIVATE_KEY', [false, 'The string value of the private key that will be used. If you are using MSFConsole, this value should be set as file:PRIVATE_KEY_PATH. OpenSSH, RSA, DSA, and ECDSA private keys are supported.']) | ||
| ], self.class | ||
| ) | ||
|
|
||
|
|
@@ -55,7 +62,7 @@ def rport | |
| datastore['RPORT'] | ||
| end | ||
|
|
||
| def session_setup(result, scanner) | ||
| def session_setup(result, scanner, used_key: false) | ||
| return unless scanner.ssh_socket | ||
|
|
||
| platform = scanner.get_platform(result.proof) | ||
|
|
@@ -67,9 +74,24 @@ def session_setup(result, scanner) | |
| 'USERPASS_FILE' => nil, | ||
| 'USER_FILE' => nil, | ||
| 'PASS_FILE' => nil, | ||
| 'USERNAME' => result.credential.public, | ||
| 'PASSWORD' => result.credential.private | ||
| 'USERNAME' => result.credential.public | ||
| } | ||
| if used_key | ||
| merge_me.merge!( | ||
| { | ||
| 'PASSWORD' => nil | ||
| } | ||
| ) | ||
| else | ||
| merge_me.merge!( | ||
| { | ||
| 'PASSWORD' => result.credential.private, | ||
| 'PRIVATE_KEY' => nil, | ||
| 'KEY_FILE' => nil | ||
| } | ||
| ) | ||
| end | ||
|
|
||
| s = start_session(self, nil, merge_me, false, sess.rstream, sess) | ||
| self.sockets.delete(scanner.ssh_socket.transport.socket) | ||
|
|
||
|
|
@@ -92,6 +114,35 @@ def run_host(ip) | |
| @ip = ip | ||
| print_brute :ip => ip, :msg => 'Starting bruteforce' | ||
|
|
||
| if datastore['USER_FILE'].blank? && datastore['USERNAME'].blank? && datastore['USERPASS_FILE'].blank? | ||
| validation_reason = 'At least one of USER_FILE, USERPASS_FILE or USERNAME must be given' | ||
| raise Msf::OptionValidateError.new( | ||
| { | ||
| 'USER_FILE' => validation_reason, | ||
| 'USERNAME' => validation_reason, | ||
| 'USERPASS_FILE' => validation_reason | ||
| } | ||
| ) | ||
| end | ||
|
|
||
| unless attempt_password_login? || attempt_pubkey_login? | ||
| validation_reason = 'At least one of KEY_PATH, PRIVATE_KEY or PASSWORD must be given' | ||
| raise Msf::OptionValidateError.new( | ||
| { | ||
| 'KEY_PATH' => validation_reason, | ||
| 'PRIVATE_KEY' => validation_reason, | ||
| 'PASSWORD' => validation_reason | ||
| } | ||
| ) | ||
| end | ||
|
|
||
| do_login_creds(ip) if attempt_password_login? | ||
| do_login_pubkey(ip) if attempt_pubkey_login? | ||
| end | ||
|
|
||
| def do_login_creds(ip) | ||
| print_status("#{ip}:#{rport} SSH - Testing User/Pass combinations") | ||
|
|
||
| cred_collection = build_credential_collection( | ||
| username: datastore['USERNAME'], | ||
| password: datastore['PASSWORD'] | ||
|
|
@@ -130,7 +181,7 @@ def run_host(ip) | |
|
|
||
| if datastore['CreateSession'] | ||
| begin | ||
| session_setup(result, scanner) | ||
| session_setup(result, scanner, used_key: false) | ||
| rescue StandardError => e | ||
| elog('Failed to setup the session', error: e) | ||
| print_brute :level => :error, :ip => ip, :msg => "Failed to setup the session - #{e.class} #{e.message}" | ||
|
|
@@ -159,4 +210,110 @@ def run_host(ip) | |
| end | ||
| end | ||
| end | ||
|
|
||
| def do_login_pubkey(ip) | ||
| print_status("#{ip}:#{rport} SSH - Testing Cleartext Keys") | ||
|
|
||
| keys = Metasploit::Framework::KeyCollection.new( | ||
| key_path: datastore['KEY_PATH'], | ||
| password: datastore['KEY_PASS'], | ||
| user_file: datastore['USER_FILE'], | ||
| username: datastore['USERNAME'], | ||
| private_key: datastore['PRIVATE_KEY'] | ||
| ) | ||
|
|
||
| unless keys.valid? | ||
| print_error('Files that failed to be read:') | ||
| keys.error_list.each do |err| | ||
| print_line("\t- #{err}") | ||
| end | ||
| end | ||
|
|
||
| keys = prepend_db_keys(keys) | ||
|
|
||
| key_count = keys.key_data.count | ||
| key_sources = [] | ||
| unless datastore['KEY_PATH'].blank? | ||
| key_sources.append(datastore['KEY_PATH']) | ||
| end | ||
|
|
||
| unless datastore['PRIVATE_KEY'].blank? | ||
| key_sources.append('PRIVATE_KEY') | ||
| end | ||
|
|
||
| print_brute level: :vstatus, ip: ip, msg: "Testing #{key_count} #{'key'.pluralize(key_count)} from #{key_sources.join(' and ')}" | ||
| scanner = Metasploit::Framework::LoginScanner::SSH.new( | ||
| configure_login_scanner( | ||
| host: ip, | ||
| port: rport, | ||
| cred_details: keys, | ||
| stop_on_success: datastore['STOP_ON_SUCCESS'], | ||
| bruteforce_speed: datastore['BRUTEFORCE_SPEED'], | ||
| proxies: datastore['Proxies'], | ||
| connection_timeout: datastore['SSH_TIMEOUT'], | ||
| framework: framework, | ||
| framework_module: self, | ||
| skip_gather_proof: !datastore['GatherProof'] | ||
| ) | ||
| ) | ||
|
|
||
| scanner.verbosity = :debug if datastore['SSH_DEBUG'] | ||
|
|
||
| scanner.scan! do |result| | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. realising this section is very similar between the two login methods bar the slight different in printing, I can probably DRY this up too |
||
| credential_data = result.to_h | ||
| credential_data.merge!( | ||
| module_fullname: self.fullname, | ||
| workspace_id: myworkspace_id | ||
| ) | ||
| case result.status | ||
| when Metasploit::Model::Login::Status::SUCCESSFUL | ||
| print_brute level: :good, ip: ip, msg: "Success: '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'" | ||
| print_brute level: :vgood, ip: ip, msg: "#{result.credential}', ' ')}'" | ||
| begin | ||
| credential_core = create_credential(credential_data) | ||
| credential_data[:core] = credential_core | ||
| create_credential_login(credential_data) | ||
| rescue ::StandardError => e | ||
| print_brute level: :info, ip: ip, msg: "Failed to create credential: #{e.class} #{e}" | ||
| print_brute level: :warn, ip: ip, msg: 'We do not currently support storing password protected SSH keys: https://github.com/rapid7/metasploit-framework/issues/20598' | ||
| end | ||
|
|
||
| if datastore['CreateSession'] | ||
| session_setup(result, scanner, used_key: true) | ||
| end | ||
| if datastore['GatherProof'] && scanner.get_platform(result.proof) == 'unknown' | ||
| msg = 'While a session may have opened, it may be bugged. If you experience issues with it, re-run this module with' | ||
| msg << " 'set gatherproof false'. Also consider submitting an issue at github.com/rapid7/metasploit-framework with" | ||
| msg << ' device details so it can be handled in the future.' | ||
| print_brute level: :error, ip: ip, msg: msg | ||
| end | ||
| :next_user | ||
| when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT | ||
| if datastore['VERBOSE'] | ||
| print_brute level: :verror, ip: ip, msg: "Could not connect: #{result.proof}" | ||
| end | ||
| scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? | ||
| invalidate_login(credential_data) | ||
| :abort | ||
| when Metasploit::Model::Login::Status::INCORRECT | ||
| if datastore['VERBOSE'] | ||
| print_brute level: :verror, ip: ip, msg: "Failed: '#{result.credential}'" | ||
| end | ||
| invalidate_login(credential_data) | ||
| scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? | ||
| else | ||
| invalidate_login(credential_data) | ||
| scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def attempt_pubkey_login? | ||
| datastore['KEY_PATH'].present? || datastore['PRIVATE_KEY'].present? | ||
| end | ||
|
|
||
| def attempt_password_login? | ||
| datastore['PASSWORD'].present? || datastore['PASS_FILE'].present? || datastore['USERPASS_FILE'].present? | ||
| end | ||
|
|
||
| end | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to avoid chaining
&&maybe something like?