-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
171 additions
and
120 deletions.
There are no files selected for viewing
This file contains 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 |
---|---|---|
|
@@ -3,159 +3,210 @@ | |
# Current source: https://github.com/rapid7/metasploit-framework | ||
## | ||
|
||
require 'net/ssh' | ||
require 'net/ssh/command_stream' | ||
require 'metasploit/framework/login_scanner/ssh' | ||
require 'metasploit/framework/credential_collection' | ||
require 'recog' | ||
require 'net/ssh/transport/session' | ||
|
||
class MetasploitModule < Msf::Auxiliary | ||
include Msf::Auxiliary::AuthBrute | ||
include Msf::Auxiliary::Report | ||
include Msf::Auxiliary::CommandShell | ||
include Msf::Auxiliary::Scanner | ||
include Msf::Exploit::Remote::SSH::Options | ||
include Msf::Auxiliary::Report | ||
|
||
def initialize | ||
super( | ||
'Name' => 'SSH Login Check Scanner', | ||
'Description' => %q{ | ||
This module will test ssh logins on a range of machines and | ||
report successful logins. If you have loaded a database plugin | ||
and connected to a database this module will record successful | ||
logins and hosts so you can track your access. | ||
}, | ||
'Author' => ['todb'], | ||
'References' => | ||
[ | ||
[ 'CVE', '1999-0502'] # Weak password | ||
], | ||
'License' => MSF_LICENSE, | ||
'DefaultOptions' => {'VERBOSE' => false} # Disable annoying connect errors | ||
'Name' => 'SSH Version Scanner', | ||
'Description' => 'Detect SSH Version, and the algorithms available from the server', | ||
'References' => [ | ||
['URL', 'https://en.wikipedia.org/wiki/SecureShell'], # general info | ||
['URL', 'https://datatracker.ietf.org/doc/html/rfc8732#name-deprecated-algorithms'], # deprecation of kex gss-sha1 stuff | ||
['URL', 'https://datatracker.ietf.org/doc/html/draft-ietf-curdle-ssh-kex-sha2-20#page-16'], # diffie-hellman-group-exchange-sha1, diffie-hellman-group1-sha1, rsa1024-sha1 | ||
['URL', 'https://datatracker.ietf.org/doc/html/rfc8758#name-iana-considerations'], # arc4 deprecation | ||
['URL', 'https://github.com/net-ssh/net-ssh?tab=readme-ov-file#supported-algorithms'] # a bunch of diff removed things from the ruby lib | ||
], | ||
'Author' => [ | ||
'Daniel van Eeden <metasploit[at]myname.nl>', # original author | ||
'h00die' # algorithms enhancements | ||
], | ||
'License' => MSF_LICENSE | ||
) | ||
|
||
register_options( | ||
[ | ||
Opt::RPORT(22) | ||
], self.class | ||
) | ||
|
||
register_advanced_options( | ||
[ | ||
Opt::Proxies, | ||
OptBool.new('SSH_DEBUG', [false, 'Enable SSH debugging output (Extreme verbosity!)', false]), | ||
OptInt.new('SSH_TIMEOUT', [false, 'Specify the maximum time to negotiate a SSH session', 30]), | ||
OptBool.new('GatherProof', [true, 'Gather proof of access via pre-session shell commands', true]) | ||
] | ||
Opt::RPORT(22), | ||
OptInt.new('TIMEOUT', [true, 'Timeout for the SSH probe', 30]) | ||
], | ||
self.class | ||
) | ||
end | ||
|
||
deregister_options('PASSWORD_SPRAY') | ||
def timeout | ||
datastore['TIMEOUT'] | ||
end | ||
|
||
def rport | ||
datastore['RPORT'] | ||
end | ||
|
||
def session_setup(result, scanner) | ||
return unless scanner.ssh_socket | ||
|
||
platform = scanner.get_platform(result.proof) | ||
|
||
# Create a new session | ||
sess = Msf::Sessions::SshCommandShellBind.new(scanner.ssh_socket) | ||
def run_host(target_host) | ||
::Timeout.timeout(timeout) do | ||
transport = Net::SSH::Transport::Session.new(target_host, { port: rport }) | ||
|
||
merge_me = { | ||
'USERPASS_FILE' => nil, | ||
'USER_FILE' => nil, | ||
'PASS_FILE' => nil, | ||
'USERNAME' => result.credential.public, | ||
'PASSWORD' => result.credential.private | ||
} | ||
s = start_session(self, nil, merge_me, false, sess.rstream, sess) | ||
self.sockets.delete(scanner.ssh_socket.transport.socket) | ||
server_data = transport.algorithms.instance_variable_get(:@server_data) | ||
host_keys = transport.algorithms.session.instance_variable_get(:@host_keys).instance_variable_get(:@host_keys) | ||
if !host_keys.empty? | ||
print_status("Key Fingerprint: #{host_keys[0].fingerprint}") | ||
end | ||
|
||
# Set the session platform | ||
s.platform = platform | ||
ident = transport.server_version.version | ||
|
||
# Create database host information | ||
host_info = {host: scanner.host} | ||
table = Rex::Text::Table.new( | ||
'Header' => 'Server Encryption', | ||
'Indent' => 2, | ||
'SortIndex' => 0, | ||
'Columns' => [ 'Type', 'Value'] | ||
) | ||
|
||
unless s.platform == 'unknown' | ||
host_info[:os_name] = s.platform | ||
end | ||
server_data[:language_server].each do |language| | ||
table << ['Language', language] | ||
end | ||
|
||
report_host(host_info) | ||
server_data[:compression_server].each do |compression| | ||
table << ['Compression', compression] | ||
end | ||
|
||
s | ||
end | ||
server_data[:encryption_server].each do |encryption| | ||
['arcfour', 'arcfour128', 'arcfour256'].each do |bad_enc| | ||
next unless encryption.downcase.start_with? bad_enc | ||
|
||
print_good("Encryption #{encryption} is deprecated and should not be used.") | ||
report_vuln( | ||
host: target_host, | ||
port: rport, | ||
proto: 'tcp', | ||
name: name, | ||
info: "Module #{fullname} confirmed SSH Encryption #{encryption} is available, but should be deprecated", | ||
refs: ['https://datatracker.ietf.org/doc/html/rfc8758#name-iana-considerations'] | ||
) | ||
end | ||
[ | ||
'aes256-cbc', 'aes192-cbc', 'aes128-cbc', '[email protected]', | ||
'blowfish-ctr blowfish-cbc', 'cast128-ctr', 'cast128-cbc', '3des-ctr', '3des-cbc', 'idea-cbc', 'none' | ||
].each do |bad_enc| | ||
next unless encryption.downcase.start_with? bad_enc | ||
|
||
print_good("Encryption #{encryption} is deprecated and should not be used.") | ||
report_vuln( | ||
host: target_host, | ||
port: rport, | ||
proto: 'tcp', | ||
name: name, | ||
info: "Module #{fullname} confirmed SSH Encryption #{encryption} is available, but should be deprecated", | ||
refs: ['https://github.com/net-ssh/net-ssh?tab=readme-ov-file#encryption-algorithms-ciphers'] | ||
) | ||
end | ||
table << ['Encryption', encryption] | ||
end | ||
|
||
server_data[:hmac_server].each do |hmac| | ||
['hmac-sha2-512-96', 'hmac-sha2-256-96', 'hmac-sha1-96', 'hmac-ripemd160', 'hmac-md5', 'hmac-md5-96', 'none'].each do |bad_hmac| | ||
next unless hmac.downcase.start_with? bad_hmac | ||
|
||
print_good("HMAC #{hmac} is deprecated and should not be used.") | ||
report_vuln( | ||
host: target_host, | ||
port: rport, | ||
proto: 'tcp', | ||
name: name, | ||
info: "Module #{fullname} confirmed SSH HMAC #{hmac} is available, but should be deprecated", | ||
refs: ['https://github.com/net-ssh/net-ssh?tab=readme-ov-file#message-authentication-code-algorithms'] | ||
) | ||
end | ||
table << ['HMAC', hmac] | ||
end | ||
|
||
def run_host(ip) | ||
@ip = ip | ||
print_brute :ip => ip, :msg => 'Starting bruteforce' | ||
server_data[:host_key].each do |host_key| | ||
['ecdsa-sha2-nistp521', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp256'].each do |bad_key| | ||
next unless host_key.downcase.start_with? bad_key | ||
|
||
print_good("Host Key Encryption #{host_key} uses a weak elliptic curve and should not be used.") | ||
report_vuln( | ||
host: target_host, | ||
port: rport, | ||
proto: 'tcp', | ||
name: name, | ||
info: "Module #{fullname} confirmed SSH Host Key Encryption #{host_key} is available, but should be deprecated", | ||
refs: ['https://github.com/net-ssh/net-ssh?tab=readme-ov-file#host-keys'] | ||
) | ||
end | ||
table << ['Host Key', host_key] | ||
end | ||
|
||
cred_collection = build_credential_collection( | ||
username: datastore['USERNAME'], | ||
password: datastore['PASSWORD'], | ||
) | ||
server_data[:kex].each do |kex| | ||
['gss-group1-sha1-', 'gss-group14-sha1-', 'gss-gex-sha1-'].each do |bad_kex| | ||
next unless kex.downcase.start_with? bad_kex | ||
|
||
print_good("Key Exchange (kex) #{kex} is deprecated and should not be used.") | ||
report_vuln( | ||
host: target_host, | ||
port: rport, | ||
proto: 'tcp', | ||
name: name, | ||
info: "Module #{fullname} confirmed SSH Encryption #{kex} is available, but should be deprecated", | ||
refs: ['https://datatracker.ietf.org/doc/html/rfc8732#name-deprecated-algorithms'] | ||
) | ||
end | ||
['ecdsa-sha2-nistp521', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp256'].each do |bad_kex| | ||
next unless kex.downcase.start_with? bad_kex | ||
|
||
print_good("Key Exchange (kex) #{kex} uses a weak elliptic curve and should not be used.") | ||
report_vuln( | ||
host: target_host, | ||
port: rport, | ||
proto: 'tcp', | ||
name: name, | ||
info: "Module #{fullname} confirmed SSH Encryption #{kex} is available, but should be deprecated", | ||
refs: ['https://github.com/net-ssh/net-ssh?tab=readme-ov-file#key-exchange'] | ||
) | ||
end | ||
['diffie-hellman-group-exchange-sha1', 'diffie-hellman-group1-sha1', 'rsa1024-sha1'].each do |bad_kex| | ||
next unless kex.downcase.start_with? bad_kex | ||
|
||
print_good("Key Exchange (kex) #{kex} is deprecated and should not be used.") | ||
report_vuln( | ||
host: target_host, | ||
port: rport, | ||
proto: 'tcp', | ||
name: name, | ||
info: "Module #{fullname} confirmed SSH Encryption #{kex} is available, but should be deprecated", | ||
refs: ['https://datatracker.ietf.org/doc/html/draft-ietf-curdle-ssh-kex-sha2-20#page-16'] | ||
) | ||
end | ||
table << ['Key Exchange (kex)', kex] | ||
end | ||
|
||
scanner = Metasploit::Framework::LoginScanner::SSH.new( | ||
host: ip, | ||
port: rport, | ||
cred_details: cred_collection, | ||
proxies: datastore['Proxies'], | ||
stop_on_success: datastore['STOP_ON_SUCCESS'], | ||
bruteforce_speed: datastore['BRUTEFORCE_SPEED'], | ||
connection_timeout: datastore['SSH_TIMEOUT'], | ||
framework: framework, | ||
framework_module: self, | ||
skip_gather_proof: !datastore['GatherProof'] | ||
) | ||
# XXX check for host key size? | ||
# https://www.tenable.com/plugins/nessus/153954 | ||
|
||
scanner.verbosity = :debug if datastore['SSH_DEBUG'] | ||
# Try to match with Recog and show the relevant fields to the user | ||
info = '' | ||
if /^SSH-\d+\.\d+-(.*)$/ =~ ident | ||
recog_match = Recog::Nizer.match('ssh.banner', ::Regexp.last_match(1)) | ||
if recog_match | ||
info << ' ( ' | ||
recog_match.each_pair do |k, v| | ||
next if k == 'matched' | ||
|
||
scanner.scan! do |result| | ||
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.credential}' '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'" | ||
credential_data[:private_type] = :password | ||
credential_core = create_credential(credential_data) | ||
credential_data[:core] = credential_core | ||
create_credential_login(credential_data) | ||
|
||
if datastore['CreateSession'] | ||
begin | ||
session_setup(result, scanner) | ||
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}" | ||
info << "#{k}=#{v} " | ||
end | ||
info << ')' | ||
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 | ||
vprint_brute :level => :verror, :ip => ip, :msg => "Could not connect: #{result.proof}" | ||
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? | ||
invalidate_login(credential_data) | ||
:abort | ||
when Metasploit::Model::Login::Status::INCORRECT | ||
vprint_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'" | ||
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 | ||
|
||
print_status("SSH server version: #{ident}#{info}") | ||
report_service(host: target_host, port: rport, name: 'ssh', proto: 'tcp', info: ident) | ||
print_status(table.to_s) | ||
end | ||
rescue EOFError, Rex::ConnectionError => e | ||
vprint_error(e.message) # This may be a little noisy, but it is consistent | ||
rescue Timeout::Error | ||
vprint_warning("Timed out after #{timeout} seconds. Skipping.") | ||
end | ||
end |