diff --git a/documentation/modules/auxiliary/scanner/ftp/ftp_login.md b/documentation/modules/auxiliary/scanner/ftp/ftp_login.md index f9eadd3a64400..4d955b2262c9f 100644 --- a/documentation/modules/auxiliary/scanner/ftp/ftp_login.md +++ b/documentation/modules/auxiliary/scanner/ftp/ftp_login.md @@ -1,12 +1,12 @@ ## Description -This module will test FTP 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. +This module tests FTP logins on a range of machines and reports successful logins. If a database is connected, successful logins, hosts, and credentials are recorded. On successful login, the module can optionally check read/write access, store a directory listing as loot, and fingerprint the server via `FEAT`, `STAT`, and `SYST`. ## Vulnerable Application -### Install ftp server on Kali Linux: +### Install a FTP server on Kali Linux -1. ```apt-get install vsftpd``` +1. ```apt-get install vsftpd``` 2. Allow local users to log in and to allow ftp uploads by editing file `/etc/vsftpd.conf` uncommenting the following: ``` @@ -16,12 +16,16 @@ This module will test FTP logins on a range of machines and report successful lo chroot_list_file=/etc/vsftpd.chroot_list ``` -3. Create the file `/etc/vsftpd.chroot_list` and add the local users you want allow to connect to FTP server. Start service and test connections: -4. ```service vsftpd start``` +3. Create `/etc/vsftpd.chroot_list` and add local users to permit. +4. Start the service: -### Installing FTP for IIS 7.5 in Windows: + ``` + service vsftpd start + ``` -#### IIS 7.5 for Windows Server 2008 R2: +### Installing FTP for IIS 7.5 on Windows + +#### IIS 7.5 for Windows Server 2008 R2 1. On the taskbar, click Start, point to Administrative Tools, and then click Server Manager. 2. In the Server Manager hierarchy pane, expand Roles, and then click Web Server (IIS). @@ -30,34 +34,98 @@ This module will test FTP logins on a range of machines and report successful lo 5. Select FTP Service. (Note: To support ASP.NET Membership or IIS Manager authentication for the FTP service, you will also need to select FTP Extensibility.) 6. Click Next. 7. On the Confirm Installation Selections page, click Install. -8. On the Results page, click Close. +8. On the Results page, click Close. -#### IIS 7.5 for Windows 7: +#### IIS 7.5 for Windows 7 1. On the taskbar, click Start, and then click Control Panel. 2. In Control Panel, click Programs and Features, and then click Turn Windows Features on or off. 3. Expand Internet Information Services, then FTP Server. 4. Select FTP Service. (Note: To support ASP.NET Membership or IIS Manager authentication for the FTP service, you will also need to select FTP Extensibility.) -5. Click OK. +5. Click OK. + +## Options + +| Option | Default | Description | +|---|---|---| +| `ANONYMOUS_LOGIN` | false | Attempt login as anonymous FTP user (uses browser-like passwords) | +| `CHECK_ACCESS` | true | Check read/write access for successful logins via `MKD`/`RMD` | +| `STORE_LOOT` | true | Retrieve and store the directory listing as loot | +| `FINGERPRINT` | false | Gather server info via `FEAT`, `STAT`, and `SYST` | ## Verification Steps 1. Do: ```use auxiliary/scanner/ftp/ftp_login``` 2. Do: ```set RHOSTS [IP]``` -3. Do: ```set RPORT [IP]``` -4. Do: ```run``` +3. Do: ```set RPORT [PORT]``` +4. Do: Either ```set USERNAME ``` and ```set PASSWORD ```, or ```set ANONYMOUS_LOGIN true``` +5. Do: ```run``` ## Scenarios +### Anonymous login against Metasploitable 2 + ``` -msf> use auxiliary/scanner/ftp/ftp_login -msf auxiliary(ftp_login) > set RHOSTS ftp.openbsd.org -msf auxiliary(ftp_login) > set USERNAME ftp -msf auxiliary(ftp_login) > set PASSWORD hello@metasploit.com -msf auxiliary(ftp_login) > run -[*] 129.128.5.191:21 - Starting FTP login sweep -[+] 129.128.5.191:21 - LOGIN SUCCESSFUL: ftp:hello@metasploit.com -[*] Scanned 1 of 1 hosts (100% complete) +msf > use auxiliary/scanner/ftp/ftp_login +msf auxiliary(scanner/ftp/ftp_login) > set RHOSTS 10.0.0.10 +RHOSTS => 10.0.0.10 +msf auxiliary(scanner/ftp/ftp_login) > set ANONYMOUS_LOGIN true +ANONYMOUS_LOGIN => true +msf auxiliary(scanner/ftp/ftp_login) > run + +[*] 10.0.0.10:21 - Getting FTP banner +[*] 10.0.0.10:21 - FTP Banner: vsFTPd 2.3.4 +[*] 10.0.0.10:21 - Starting FTP login sweep +[*] 10.0.0.10:21 - Checking read/write access +[*] 10.0.0.10:21 - Listing directory contents +[*] 10.0.0.10:21 - Directory listing: (empty) +[+] 10.0.0.10:21 - Login Successful: anonymous:mozilla@example.com (Read-only) +[*] 10.0.0.10:21 - Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +``` + +### Credential brute-force against Metasploitable 2 + +``` +msf > use auxiliary/scanner/ftp/ftp_login +msf auxiliary(scanner/ftp/ftp_login) > set RHOSTS 10.0.0.10 +RHOSTS => 10.0.0.10 +msf auxiliary(scanner/ftp/ftp_login) > set USERNAME msfadmin +USERNAME => msfadmin +msf auxiliary(scanner/ftp/ftp_login) > set PASSWORD msfadmin +PASSWORD => msfadmin +msf auxiliary(scanner/ftp/ftp_login) > set STOP_ON_SUCCESS true +STOP_ON_SUCCESS => true +msf auxiliary(scanner/ftp/ftp_login) > set FINGERPRINT true +STOP_ON_SUCCESS => true +msf auxiliary(scanner/ftp/ftp_login) > run + +[*] 10.0.0.10:21 - Getting FTP banner +[*] 10.0.0.10:21 - FTP Banner: vsFTPd 2.3.4 +[*] 10.0.0.10:21 - Starting FTP login sweep +[*] 10.0.0.10:21 - Checking read/write access +[*] 10.0.0.10:21 - Listing directory contents +[*] 10.0.0.10:21 - Directory listing: +drwxr-xr-x 6 1000 1000 4096 Apr 28 2010 vulnerable +[+] 10.0.0.10:21 - Directory listing stored to: /home/kali/.msf4/loot/20260507170404_default_10.0.0.10_ftp.dir_listing_925538.txt +[*] 10.0.0.10:21 - Fingerprinting FTP service +[*] 10.0.0.10:21 - Sending FTP command: FEAT +[*] 10.0.0.10:21 - FTP FEAT: 211-Features: + EPRT + EPSV + MDTM + PASV + REST STREAM + SIZE + TVFS + UTF8 +211 End +[*] 10.0.0.10:21 - Sending FTP command: STAT +[*] 10.0.0.10:21 - FTP STAT: 211-FTP server status: + Connected to 10.0.0.1 +[*] 10.0.0.10:21 - Sending FTP command: SYST +[*] 10.0.0.10:21 - FTP SYST: Logged in as msfadmin +[+] 10.0.0.10:21 - Login Successful: msfadmin:msfadmin (Read/Write) +[*] 10.0.0.10:21 - Scanned 1 of 1 hosts (100% complete) [*] Auxiliary module execution completed -msf auxiliary(ftp_login) > ``` diff --git a/lib/metasploit/framework/login_scanner/ftp.rb b/lib/metasploit/framework/login_scanner/ftp.rb index 27284e91704cd..f4fecebbf1ce2 100644 --- a/lib/metasploit/framework/login_scanner/ftp.rb +++ b/lib/metasploit/framework/login_scanner/ftp.rb @@ -20,6 +20,8 @@ class FTP PRIVATE_TYPES = [ :password ] REALM_KEY = nil + public :banner + # @!attribute ftp_timeout # @return [Integer] The timeout in seconds to wait for a response to an FTP command attr_accessor :ftp_timeout @@ -40,20 +42,17 @@ def attempt_login(credential) } begin - success = connect_login(credential.public, credential.private) + ftpsock = connect(true) + res = send_user(credential.public, ftpsock) + res = send_pass(credential.private, ftpsock) if res =~ /^(331|2)/ + result_options[:proof] = res.to_s.strip + result_options[:status] = res =~ /^2/ ? Metasploit::Model::Login::Status::SUCCESSFUL : Metasploit::Model::Login::Status::INCORRECT rescue ::EOFError, Errno::ECONNRESET, Rex::ConnectionError, Rex::ConnectionTimeout, ::Timeout::Error result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT - success = false ensure disconnect end - if success - result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL - elsif !(result_options.has_key? :status) - result_options[:status] = Metasploit::Model::Login::Status::INCORRECT - end - result = ::Metasploit::Framework::LoginScanner::Result.new(result_options) result.host = host result.port = port diff --git a/modules/auxiliary/scanner/ftp/ftp_login.rb b/modules/auxiliary/scanner/ftp/ftp_login.rb index 093eb163a587f..6d2ca85a797a2 100644 --- a/modules/auxiliary/scanner/ftp/ftp_login.rb +++ b/modules/auxiliary/scanner/ftp/ftp_login.rb @@ -9,7 +9,6 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::Ftp include Msf::Auxiliary::Scanner - include Msf::Auxiliary::Report include Msf::Auxiliary::AuthBrute def proto @@ -21,15 +20,20 @@ def initialize 'Name' => 'FTP Authentication Scanner', 'Description' => %q{ This module will test FTP logins on a range of machines and - report successful logins. If you have loaded a database plugin + 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 + [ 'CVE', '1999-0502' ] # Weak password ], 'License' => MSF_LICENSE, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS, ACCOUNT_LOCKOUTS], + 'Reliability' => [] + }, 'DefaultOptions' => { 'ConnectTimeout' => 30 } @@ -39,29 +43,79 @@ def initialize [ Opt::Proxies, Opt::RPORT(21), - OptBool.new('RECORD_GUEST', [ false, "Record anonymous/guest logins to the database", false]) + OptBool.new('ANONYMOUS_LOGIN', [ false, 'Attempt to login as an anonymous FTP user', false ]), # Overwrite the AuthBrute mixin, as its not sending blank/empty user/pass + OptBool.new('CHECK_ACCESS', [ false, 'Check READ/WRITE access for successful logins', true ]), + OptBool.new('STORE_LOOT', [false, 'Store the directory listing as loot', true]), + OptBool.new('FINGERPRINT', [false, 'Gather server info via FEAT, STAT and SYST', false]) ] ) register_advanced_options( [ - OptBool.new('SINGLE_SESSION', [ false, 'Disconnect after every login attempt', false]), + OptBool.new('SINGLE_SESSION', [ false, 'Disconnect after every login attempt', false ]), ] ) deregister_options('FTPUSER', 'FTPPASS') # Can use these, but should use 'username' and 'password' - @accepts_all_logins = {} + end + + + def get_loot(username = 'anonymous') + vprint_status('Listing directory contents') + + username = username.downcase + + listing = send_cmd_data(['LS'], nil) + if listing.nil? + print_warning('Could not retrieve directory listing (data connection failed)') + elsif listing[1].nil? || listing[1].empty? + vprint_status('Directory listing: (empty)') + else + vprint_status("Directory listing:\n#{listing[1]}") + path = store_loot('ftp.dir_listing', 'text/plain', rhost, listing[1], "ftp_#{username}.txt", "FTP directory listing for #{username}") + print_good("Directory listing stored to: #{path}") + end + end + + def fingerprint_server(username = 'anonymous') + print_status('Fingerprinting FTP service') + + [ + ['FEAT', 'ftp.cmd.feat'], # server-level + ['STAT', 'ftp.cmd.stat'], # user-level + ['SYST', 'ftp.cmd.syst'] # server-level + ].each do |cmd, note_type| + vprint_status("Sending FTP command: #{cmd}") + response = send_cmd([cmd], true).to_s + next if response.empty? + + print_status("FTP #{cmd}: #{response.strip}") + report_note( + host: rhost, + port: rport, + proto: 'tcp', + sname: 'ftp', + type: note_type, + data: { username: username, output: response.strip } + ) + end end def run_host(ip) - print_status("#{ip}:#{rport} - Starting FTP login sweep") + print_status('Starting FTP login sweep') cred_collection = build_credential_collection( username: datastore['USERNAME'], password: datastore['PASSWORD'], - prepended_creds: anonymous_creds + prepended_creds: anonymous_creds, + anonymous_login: false # Otherwise this would send blank for both user/password, so its different to anonymous_creds() ) + if cred_collection.empty? + print_error('No credentials specified. Set USERNAME/PASSWORD, USER_FILE/PASS_FILE, or ANONYMOUS_LOGIN.') + return + end + scanner = Metasploit::Framework::LoginScanner::FTP.new( configure_login_scanner( host: ip, @@ -86,46 +140,90 @@ def run_host(ip) ) scanner.scan! do |result| + unless banner_reported + banner_reported = true + if scanner.banner&.match?(/^(120|220)[\s-]/) + self.banner = scanner.banner + vprint_status("FTP Banner: #{banner_version}") + report_service(host: rhost, port: rport, proto: 'tcp', name: 'ftp', info: Rex::Text.to_hex_ascii(banner_version)) + report_note(host: rhost, port: rport, proto: 'tcp', sname: 'ftp', type: 'ftp.banner', data: { banner: Rex::Text.to_hex_ascii(scanner.banner.strip) }) + end + end + credential_data = result.to_h credential_data.merge!( - module_fullname: self.fullname, + module_fullname: fullname, workspace_id: myworkspace_id ) if result.success? credential_data[:private_type] = :password credential_core = create_credential(credential_data) credential_data[:core] = credential_core - create_credential_login(credential_data) - print_good "#{ip}:#{rport} - Login Successful: #{result.credential}" + if datastore['CHECK_ACCESS'] || datastore['STORE_LOOT'] || datastore['FINGERPRINT'] + begin + connect(true, false) + send_user(result.credential.public) + if send_pass(result.credential.private).start_with?('2') + if datastore['CHECK_ACCESS'] + vprint_status('Checking read/write access') + access_level = test_ftp_access + end + + get_loot(result.credential.public) if datastore['STORE_LOOT'] + + fingerprint_server(result.credential.public) if datastore['FINGERPRINT'] + end + rescue ::Rex::ConnectionError, ::Rex::ConnectionTimeout, ::EOFError, Errno::ECONNRESET, ::Timeout::Error, ::IOError => e + vprint_error(e.message) + ensure + disconnect + end + end + + credential_data[:access_level] = access_level if access_level + + msg = "Login Successful: #{result.credential}" + msg << " (#{access_level})" if access_level + print_good(msg) + + report_vuln( + host: rhost, + port: rport, + proto: 'tcp', + name: 'Weak FTP Credentials', + info: "Login accepted: #{result.credential}#{" (#{access_level})" if access_level}", + refs: references + ) + + create_credential_login(credential_data) else + proof = result.proof.to_s.strip + proof_str = proof.empty? ? result.status.to_s : "#{result.status}: #{proof}" + vprint_error("Login Failed: #{result.credential} (#{proof_str})") + invalidate_login(credential_data) - vprint_error "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" end end end - # Always check for anonymous access by pretending to be a browser. + # Check for anonymous access by pretending to be a browser 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 + return [] unless datastore['ANONYMOUS_LOGIN'] + + ['mozilla@example.com', 'IEUser@', 'User@', 'chrome@example.com'].map do |password| + Metasploit::Framework::Credential.new(public: 'anonymous', private: password) end - anon_creds end - def test_ftp_access(user, scanner) + def test_ftp_access dir = Rex::Text.rand_text_alpha(8) - write_check = scanner.send_cmd(['MKD', dir], true) - if write_check and write_check =~ /^2/ - scanner.send_cmd(['RMD', dir], true) - print_status("#{rhost}:#{rport} - User '#{user}' has READ/WRITE access") - return 'Read/Write' + write_check = send_cmd(['MKD', dir], true) + if write_check && write_check.start_with?('2') + send_cmd(['RMD', dir], true) + 'Read/Write' else - print_status("#{rhost}:#{rport} - User '#{user}' has READ access") - return 'Read-only' + 'Read-only' end end