Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 146 additions & 3 deletions lib/msf/core/exploit/remote/ftp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def connect(global = true, verbose = nil)

# Wait for a banner to arrive...
self.banner = recv_ftp_resp(fd)
remove_instance_variable(:@recog_banner) if instance_variable_defined?(:@recog_banner)

print_status('Connected to target FTP server') if verbose

Expand Down Expand Up @@ -90,23 +91,165 @@ def connect(global = true, verbose = nil)
type: 'ftp.banner',
data: { banner: Rex::Text.to_hex_ascii(self.banner.strip) }
)

# Lookup FTP banner
info = recog_banner
Comment thread
g0tmi1k marked this conversation as resolved.
if info
os_info = {}
info.each_pair do |k, v|
case k
when 'os.product' then os_info[:os_name] = v
when 'os.vendor' then os_info[:os_flavor] = v
when 'os.version' then os_info[:os_sp] = v
when 'os.cpe23', 'service.cpe23'
report_note(
host: rhost,
port: rport,
proto: 'tcp',
sname: 'ftp',
type: 'ftp.cpe',
data: { cpe: v },
update: :unique_data
)
end
end

report_host({ host: rhost }.merge(os_info)) unless os_info.empty?
end
end

# Return the file descriptor to the caller
fd
end

# Extracts a normalized version string from the FTP banner
# 220 (vsFTPd 2.3.4)\x0d\x0a -> vsFTPd 2.3.4
# 220 ProFTPD 1.3.1 Server (Debian) [::ffff:10.0.0.10]\x0d\x0a -> ProFTPD 1.3.1 Server (Debian)
#
# Matches the FTP banner against the Recog ftp.banner fingerprint set
# Tests each line of a multi-line banner after stripping the reply code prefix
# Result is memoised
#
# @return [Hash, nil] Recog match, or nil if no fingerprint matched
def recog_banner
Comment thread
g0tmi1k marked this conversation as resolved.
return nil unless banner
return @recog_banner if instance_variable_defined?(:@recog_banner)
Comment thread
g0tmi1k marked this conversation as resolved.

@recog_banner = nil
banner.to_s.each_line do |line|
Comment thread
g0tmi1k marked this conversation as resolved.
text = line.sub(/^\d{3}[\s-]/, '').strip
next if text.empty?
match = Recog::Nizer.match('ftp.banner', text)
@recog_banner = match and break if match
end
@recog_banner
end

#
# Returns a normalised version string from the FTP banner
# Uses Recog - falls back to regex if no match
#
# @return [String, nil] version string, or nil if no banner
def banner_version
info = recog_banner
return [info['service.product'], info['service.version']].compact.join(' ') if info

# 220 (vsFTPd 2.3.4)\x0d\x0a -> vsFTPd 2.3.4
# 220 ProFTPD 1.3.1 Server (Debian) [::ffff:192.0.2.10]\x0d\x0a -> ProFTPD 1.3.1 Server (Debian)
banner.to_s
.sub(/^\d{3}[\s-]/, '')
.strip
.gsub(/\A\(|\)\z/, '')
.gsub(/\s*\[(?:(?:\d{1,3}\.){3}\d{1,3}|[0-9A-Fa-f:]*:[0-9A-Fa-f:.]+)\]/, '')
end

#
# Sends FEAT, STAT, and SYST and records the output as workspace notes
# Skips SYST-based OS detection if Recog already identified os.product
#
# @param logged_in_as [String] current FTP session user, recorded in notes
# @raise [Rex::ConnectionError] if the server closes the connection mid-fingerprint
# @return [void]
Comment thread
g0tmi1k marked this conversation as resolved.
def ftp_fingerprint(logged_in_as: 'anonymous')
print_status("Fingerprinting FTP service (as #{logged_in_as})")

[
['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)
raise Rex::ConnectionError.new(rhost, rport) unless response
next if response.empty?

response.strip.each_line.with_index do |line, i|
prefix = i == 0 ? "FTP #{cmd}: " : ' '
vprint_status("#{prefix}#{line.strip}")
end

# Examples:
# 215 UNIX Type: L8
# 215 Windows_NT
# 215 UNIX emulated by FileZilla (Windows host but looks like *nix service)
if cmd == 'SYST'
os_name = if response.match?(/emulated/i) then nil
Comment thread
g0tmi1k marked this conversation as resolved.
elsif response.match?(/Windows_NT/i) then 'Windows'
elsif response.match?(/UNIX/i) then '*nix'
else nil
end
report_host(host: rhost, os_name: os_name) if os_name && !recog_banner&.key?('os.product')
end

report_note(
host: rhost,
port: rport,
proto: 'tcp',
sname: 'ftp',
type: note_type,
data: {
username: logged_in_as,
output: response.strip
}
)
end
end

#
# Lists the current remote directory and optionally stores the result as loot
#
# @param logged_in_as [String] current FTP session user, used to label the loot file
# @param save_loot [Boolean] store the directory listing as loot
# @return [String, nil] directory listing content, or nil on failure
def ftp_list_directory(logged_in_as: 'anonymous', save_loot: false)
print_status('Getting FTP root directory contents')

listing = send_cmd_data(['LS'], nil)
if listing.nil?
print_warning('Could not retrieve directory listing (data connection failed)')
return nil
elsif listing[1].blank?
vprint_status('Directory listing: (empty)')
return nil
end

vprint_status('Directory listing:')
listing[1].strip.each_line do |line|
vprint_status(" #{line.strip}")
end

return listing[1] unless save_loot

path = store_loot(
'ftp.dir_listing',
'text/plain',
rhost,
listing[1],
"ftp_#{logged_in_as.to_s.downcase}.txt",
"FTP directory listing for #{logged_in_as}"
)
print_good("Directory listing stored to: #{path}")

listing[1]
end

#
# This method handles establishing datasocket for data channel
#
Expand Down
Loading
Loading