Merge pull request #21372 from g0tmi1k/ftp_anonymous

ftp_anonymous: Report service/vuln, store loot & update metadata
This commit is contained in:
Christophe De La Fuente
2026-05-06 15:16:23 +02:00
committed by GitHub
3 changed files with 140 additions and 107 deletions
@@ -52,7 +52,7 @@ This module allows us to scan through a series of IP Addresses and provide detai
## Verification Steps
1. Do: ```use auxiliary/scanner/ftp/anonymous```
1. Do: ```use auxiliary/scanner/ftp/ftp_anonymous```
2. Do: ```set RHOSTS [IP]```
3. Do: ```set RPORT [IP]```
4. Do: ```run```
@@ -62,17 +62,17 @@ This module allows us to scan through a series of IP Addresses and provide detai
### vsFTPd 3.0.3 on Kali
```
msf > use auxiliary/scanner/ftp/anonymous
msf auxiliary(anonymous) > set RHOSTS 127.0.0.1
msf > use auxiliary/scanner/ftp/ftp_anonymous
msf auxiliary(ftp_anonymous) > set RHOSTS 127.0.0.1
RHOSTS => 127.0.0.1
msf auxiliary(anonymous) > set RPORT 21
msf auxiliary(ftp_anonymous) > set RPORT 21
RPORT => 21
msf auxiliary(anonymous) > exploit
msf auxiliary(ftp_anonymous) > exploit
[+] 127.0.0.1:21 - 127.0.0.1:21 - Anonymous READ (220 (vsFTPd 3.0.3))
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf auxiliary(anonymous) >
msf auxiliary(ftp_anonymous) >
```
## Confirming using NMAP
-101
View File
@@ -1,101 +0,0 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::Ftp
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
def initialize
super(
'Name' => 'Anonymous FTP Access Detection',
'Description' => 'Detect anonymous (read/write) FTP server access.',
'References' => [
['URL', 'https://en.wikipedia.org/wiki/File_Transfer_Protocol#Anonymous_FTP'],
],
'Author' => 'Matteo Cantoni <goony[at]nothink.org>',
'License' => MSF_LICENSE
)
register_options(
[
Opt::RPORT(21),
]
)
end
def run_host(target_host)
begin
res = connect_login(true, false)
banner.strip! if banner
dir = Rex::Text.rand_text_alpha(8)
if res
write_check = send_cmd(['MKD', dir], true)
if write_check && write_check =~ /^2/
send_cmd(['RMD', dir], true)
print_good("#{target_host}:#{rport} - Anonymous READ/WRITE (#{banner})")
access_type = 'Read/Write'
else
print_good("#{target_host}:#{rport} - Anonymous READ (#{banner})")
access_type = 'Read-only'
end
register_creds(target_host, access_type)
elsif banner
report_service(
host: rhost,
port: rport,
proto: 'tcp',
name: 'ftp',
info: banner
)
end
disconnect
rescue ::Interrupt
raise $ERROR_INFO
rescue ::Rex::ConnectionError, ::IOError
end
end
def register_creds(target_host, access_type)
# Build service information
service_data = {
address: target_host,
port: datastore['RPORT'],
service_name: 'ftp',
protocol: 'tcp',
workspace_id: myworkspace_id
}
# Build credential information
credential_data = {
origin_type: :service,
module_fullname: self.fullname,
private_data: datastore['FTPPASS'],
private_type: :password,
username: datastore['FTPUSER'],
workspace_id: myworkspace_id
}
credential_data.merge!(service_data)
credential_core = create_credential(credential_data)
# Assemble the options hash for creating the Metasploit::Credential::Login object
login_data = {
access_level: access_type,
core: credential_core,
last_attempted_at: DateTime.now,
status: Metasploit::Model::Login::Status::SUCCESSFUL,
workspace_id: myworkspace_id
}
login_data.merge!(service_data)
create_credential_login(login_data)
end
end
@@ -0,0 +1,134 @@
# frozen_string_literal: true
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::Ftp
include Msf::Auxiliary::Scanner
include Msf::Module::Deprecated
moved_from 'auxiliary/scanner/ftp/anonymous'
def initialize
super(
'Name' => 'Anonymous FTP Access Detection',
'Description' => 'Detect anonymous (read/write) FTP service access.',
'References' => [
['URL', 'https://en.wikipedia.org/wiki/File_Transfer_Protocol#Anonymous_FTP'],
['CVE', '1999-0497'],
],
'Author' => [
'Matteo Cantoni <goony[at]nothink.org>',
'g0tmi1k' # @g0tmi1k - additional features
],
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
register_options(
[
Opt::RPORT(21),
OptBool.new('STORE_LOOT', [false, 'Store the directory listing as loot', true])
]
)
end
def run_host(target_host)
res = connect_login(true, false)
if res
dir = Rex::Text.rand_text_alpha(8)
vprint_status("Testing write access, creating test directory: #{dir}")
# Alt would be to use STOR
write_check = send_cmd(['MKD', dir], true)
if write_check && write_check =~ /^2/
access_type = 'Read/Write'
vprint_status("Removing test directory: #{dir}")
send_cmd(['RMD', dir], true)
else
access_type = 'Read-only'
end
print_good("Anonymous #{access_type} access (#{@banner_version})")
if datastore['STORE_LOOT']
vprint_status('Listing directory contents')
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.anonymous', 'text/plain', rhost, listing[1], 'ftp_anonymous.txt', 'Anonymous FTP directory listing')
print_good("Directory listing stored to: #{path}")
end
end
report_vuln(
host: rhost,
port: rport,
proto: 'tcp',
sname: 'ftp',
name: 'Anonymous FTP Access',
info: "Anonymous FTP login accepted with #{access_type} access",
refs: references
)
register_creds(target_host, access_type)
elsif banner
print_warning("FTP service, but no anonymous access (#{@banner_version})")
else
vprint_warning('No FTP banner received')
end
rescue ::Rex::TimeoutError, ::Rex::ConnectionError, ::EOFError, ::Errno::ECONNREFUSED => e
vprint_error(e.message)
report_host(host: rhost)
rescue ::Interrupt
raise $ERROR_INFO
ensure
disconnect
end
def register_creds(target_host, access_type)
# Build service information
service_data = {
address: target_host,
port: rport,
service_name: 'ftp',
protocol: 'tcp',
workspace_id: myworkspace_id
}
# Build credential information
credential_data = {
origin_type: :service,
module_fullname: fullname,
private_data: datastore['FTPPASS'],
private_type: :password,
username: datastore['FTPUSER'],
workspace_id: myworkspace_id
}
credential_data.merge!(service_data)
credential_core = create_credential(credential_data)
# Assemble the options hash for creating the Metasploit::Credential::Login object
login_data = {
access_level: access_type,
core: credential_core,
last_attempted_at: DateTime.now,
status: Metasploit::Model::Login::Status::SUCCESSFUL,
workspace_id: myworkspace_id
}
login_data.merge!(service_data)
create_credential_login(login_data)
end
end