Merge pull request #67 from rapid7/feature/MSP-9695/ftp_login
Access level string clarified, specs passing, valid looking cores with proper info MSP-9695 #landbug/bundler_fix
commit
430b3d181e
|
@ -4,6 +4,8 @@
|
|||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'metasploit/framework/credential_collection'
|
||||
require 'metasploit/framework/login_scanner/ftp'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
|
@ -52,134 +54,98 @@ class Metasploit3 < Msf::Auxiliary
|
|||
|
||||
def run_host(ip)
|
||||
print_status("#{ip}:#{rport} - Starting FTP login sweep")
|
||||
if check_banner
|
||||
@@credentials_tried = {}
|
||||
if datastore['RECORD_GUEST'] == false and check_anonymous == :next_user
|
||||
@accepts_all_logins[@access] ||= []
|
||||
@accepts_all_logins[@access] << ip
|
||||
print_status("Successful authentication with #{@access.to_s} access on #{ip} will not be reported")
|
||||
|
||||
cred_collection = Metasploit::Framework::CredentialCollection.new(
|
||||
blank_passwords: datastore['BLANK_PASSWORDS'],
|
||||
pass_file: datastore['PASS_FILE'],
|
||||
password: datastore['PASSWORD'],
|
||||
user_file: datastore['USER_FILE'],
|
||||
userpass_file: datastore['USERPASS_FILE'],
|
||||
username: datastore['USERNAME'],
|
||||
user_as_pass: datastore['USER_AS_PASS'],
|
||||
prepended_creds: anonymous_creds
|
||||
)
|
||||
|
||||
scanner = Metasploit::Framework::LoginScanner::FTP.new(
|
||||
host: ip,
|
||||
port: rport,
|
||||
proxies: datastore['PROXIES'],
|
||||
cred_details: cred_collection,
|
||||
stop_on_success: datastore['STOP_ON_SUCCESS'],
|
||||
connection_timeout: 30
|
||||
)
|
||||
|
||||
service_data = {
|
||||
address: ip,
|
||||
port: rport,
|
||||
service_name: 'ftp',
|
||||
protocol: 'tcp',
|
||||
workspace_id: myworkspace_id
|
||||
}
|
||||
|
||||
scanner.scan! do |result|
|
||||
if result.success?
|
||||
credential_data = {
|
||||
module_fullname: self.fullname,
|
||||
origin_type: :service,
|
||||
private_data: result.credential.private,
|
||||
private_type: :password,
|
||||
username: result.credential.public
|
||||
}
|
||||
credential_data.merge!(service_data)
|
||||
|
||||
credential_core = create_credential(credential_data)
|
||||
|
||||
login_data = {
|
||||
access_level: test_ftp_access(result.credential.public, scanner),
|
||||
core: credential_core,
|
||||
last_attempted_at: DateTime.now,
|
||||
status: Metasploit::Credential::Login::Status::SUCCESSFUL
|
||||
}
|
||||
login_data.merge!(service_data)
|
||||
|
||||
create_credential_login(login_data)
|
||||
print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}"
|
||||
else
|
||||
invalidate_login(
|
||||
address: ip,
|
||||
port: rport,
|
||||
protocol: 'tcp',
|
||||
public: result.credential.public,
|
||||
private: result.credential.private,
|
||||
realm_key: nil,
|
||||
realm_value: nil,
|
||||
status: result.status)
|
||||
print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})"
|
||||
end
|
||||
each_user_pass { |user, pass|
|
||||
next if user.nil?
|
||||
ret = do_login(user,pass)
|
||||
ftp_quit if datastore['SINGLE_SESSION']
|
||||
if ret == :next_user
|
||||
unless user == user.downcase
|
||||
ret = do_login(user.downcase,pass)
|
||||
if ret == :next_user
|
||||
user = user.downcase
|
||||
print_status("Username #{user} is not case sensitive")
|
||||
end
|
||||
end
|
||||
if datastore['RECORD_GUEST']
|
||||
report_ftp_creds(user,pass,@access)
|
||||
else
|
||||
if @accepts_all_logins[@access]
|
||||
report_ftp_creds(user,pass,@access) unless @accepts_all_logins[@access].include?(ip)
|
||||
else
|
||||
report_ftp_creds(user,pass,@access)
|
||||
end
|
||||
end
|
||||
end
|
||||
ret
|
||||
}
|
||||
# check_anonymous
|
||||
else
|
||||
return
|
||||
end
|
||||
ftp_quit
|
||||
|
||||
end
|
||||
|
||||
def ftp_quit
|
||||
begin
|
||||
send_quit if @ftp_sock
|
||||
rescue ::Rex::ConnectionError, EOFError, ::Errno::ECONNRESET
|
||||
end
|
||||
disconnect if @ftp_sock
|
||||
@ftp_sock = nil
|
||||
end
|
||||
|
||||
# Always check for anonymous access by pretending to be a browser.
|
||||
def check_anonymous
|
||||
browser_passwords = {}
|
||||
browser_passwords['IE6'] = "IEUser@"
|
||||
browser_passwords['IE8'] = "User@"
|
||||
browser_passwords['Firefox'] = 'mozilla@example.com'
|
||||
browser_passwords['Chrome'] = 'chrome@example.com'
|
||||
unless @@credentials_tried.keys.include? "#{rhost}:#{rport}:anonymous"
|
||||
do_login("anonymous",browser_passwords.values[rand(browser_passwords.size)])
|
||||
end
|
||||
end
|
||||
|
||||
def check_banner
|
||||
@ftp_sock = connect(true, false)
|
||||
if self.banner
|
||||
banner_sanitized = Rex::Text.to_hex_ascii(self.banner.to_s)
|
||||
print_status("#{rhost}:#{rport} - FTP Banner: '#{banner_sanitized}'")
|
||||
report_service(:host => rhost, :port => rport, :name => "ftp", :info => banner_sanitized)
|
||||
return true
|
||||
else
|
||||
print_error("#{rhost}:#{rport} - Did not get an FTP service banner")
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def do_login(user=nil,pass=nil)
|
||||
vprint_status("#{rhost}:#{rport} - Attempting FTP login for '#{user}':'#{pass}'")
|
||||
this_attempt ||= {}
|
||||
this_attempt[[user,pass]] ||= 0
|
||||
while this_attempt[[user,pass]] <= 3
|
||||
@ftp_sock = connect(true,false) unless @ftp_sock
|
||||
begin
|
||||
user_response = send_user(user, @ftp_sock)
|
||||
if user_response !~ /^(331|2)/
|
||||
vprint_error("#{rhost}:#{rport} - The server rejected username: '#{user}'")
|
||||
return :skip_user
|
||||
end
|
||||
pass_response = send_pass(pass, @ftp_sock)
|
||||
if pass_response =~ /^2/
|
||||
print_good("#{rhost}:#{rport} - Successful FTP login for '#{user}':'#{pass}'")
|
||||
@access = test_ftp_access(user)
|
||||
ftp_quit
|
||||
return :next_user
|
||||
else
|
||||
vprint_status("#{rhost}:#{rport} - Failed FTP login for '#{user}':'#{pass}'")
|
||||
return :fail
|
||||
end
|
||||
rescue ::Rex::ConnectionError, EOFError, ::Errno::ECONNRESET => e
|
||||
this_attempt[[user,pass]] += 1
|
||||
vprint_error "#{rhost}:#{rport} - Caught #{e.class}, reconnecting and retrying"
|
||||
disconnect
|
||||
@ftp_sock = nil
|
||||
def anonymous_creds
|
||||
anon_creds = [ ]
|
||||
if datastore['RECORD_GUEST']
|
||||
['IEUser@', 'User@', 'mozilla@example.com', 'chrome@example.com' ].each do |password|
|
||||
anon_creds << Metasploit::Framework::Credential.new(public: 'anonymous', private: password)
|
||||
end
|
||||
end
|
||||
return :connection_error
|
||||
anon_creds
|
||||
end
|
||||
|
||||
def test_ftp_access(user)
|
||||
def test_ftp_access(user,scanner)
|
||||
dir = Rex::Text.rand_text_alpha(8)
|
||||
write_check = send_cmd(['MKD', dir], true)
|
||||
write_check = scanner.send_cmd(['MKD', dir], true)
|
||||
if write_check and write_check =~ /^2/
|
||||
send_cmd(['RMD',dir], true)
|
||||
scanner.send_cmd(['RMD',dir], true)
|
||||
print_status("#{rhost}:#{rport} - User '#{user}' has READ/WRITE access")
|
||||
return :write
|
||||
return 'Read/Write'
|
||||
else
|
||||
print_status("#{rhost}:#{rport} - User '#{user}' has READ access")
|
||||
return :read
|
||||
return 'Read-only'
|
||||
end
|
||||
end
|
||||
|
||||
def report_ftp_creds(user,pass,access)
|
||||
report_auth_info(
|
||||
:host => rhost,
|
||||
:port => rport,
|
||||
:sname => 'ftp',
|
||||
:user => user,
|
||||
:pass => pass,
|
||||
:type => "password#{access == :read ? "_ro" : "" }",
|
||||
:source_type => "user_supplied",
|
||||
:active => true
|
||||
)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue