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

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::Tcp
  include Msf::Auxiliary::Scanner
  include Msf::Auxiliary::Report

  def initialize
    super(
      'Name'        => 'Finger Service User Enumerator',
      'Description' => 'Identify valid users through the finger service using a variety of tricks',
      'Author'      => 'hdm',
      'License'     => MSF_LICENSE
    )
    register_options([
      Opt::RPORT(79),
      OptString.new('USERS_FILE',
        [ true, 'The file that contains a list of default UNIX accounts.',
          File.join(Msf::Config.install_root, 'data', 'wordlists', 'unix_users.txt')
        ]
      )])
  end

  def run_host(ip)
    @users = {}

    begin
      vprint_status "#{rhost}:#{rport} - Sending empty finger request."
      finger_empty
      vprint_status "#{rhost}:#{rport} - Sending test finger requests."
      finger_zero
      finger_dot
      finger_chars
      vprint_status "#{rhost}:#{rport} - Sending finger request for #{finger_user_common.count} users"
      finger_list

    rescue ::Rex::ConnectionError
    rescue ::Exception => e
      print_error("#{e} #{e.backtrace}")
    end
    report_service(:host => rhost, :port => rport, :name => "finger")

    if(@users.empty?)
      print_status("#{ip}:#{rport} No users found.")
    else
      print_good("#{ip}:#{rport} Users found: #{@users.keys.sort.join(", ")}")
      report_note(
        :host => rhost,
        :port => rport,
        :type => 'finger.users',
        :data => {:users => @users.keys}
      )
    end
  end


  def finger_empty
    connect
    sock.put("\r\n")
    buff = finger_slurp_data
    parse_users(buff)
    disconnect
  end

  def finger_zero
    connect
    sock.put("0\r\n")
    buff = finger_slurp_data
    parse_users(buff)
    disconnect
  end

  def finger_dot
    connect
    sock.put(".\r\n")
    buff = finger_slurp_data
    parse_users(buff)
    disconnect
  end

  def finger_chars
    connect
    sock.put("m m m m m m m m\r\n")
    buff = finger_slurp_data
    if buff.scan(/\r?\nm\s/).size > 7
      @multiple_requests = true
      vprint_status "#{rhost}:#{rport} - Multiple users per request is okay."
    end
    parse_users(buff)
    disconnect
  end

  def finger_list
    if !@multiple_requests
      finger_user_common.each do |user|
        next if @users[user]
        connect
        vprint_status "#{rhost}:#{rport} - Sending finger request for #{user}..."
        sock.put("#{user}\r\n")
        buff = finger_slurp_data
        ret = parse_users(buff)
        disconnect
        break if not ret
      end
    else
      while !finger_user_common.empty?
        user_batch = []
        while user_batch.size < 8 and !finger_user_common.empty?
          new_user = finger_user_common.shift
          next if @users.keys.include? new_user
          user_batch << new_user
        end
        connect
        vprint_status "#{rhost}:#{rport} - Sending finger request for #{user_batch.join(", ")}..."
        sock.put("#{user_batch.join(" ")}\r\n")
        buff = finger_slurp_data
        ret = parse_users(buff)
        disconnect
        break if not ret
      end
    end
  end

  def finger_slurp_data
    buff = ""
    begin
      while(res = sock.get_once(-1, 5) || '')
        buff << res
        break if buff.length > (1024*1024)
      end
    rescue ::Interrupt
      raise $!
    rescue ::Exception
    end
    buff
  end

  def finger_user_common
    if(! @common)
      File.open(datastore['USERS_FILE'], "rb") do |fd|
        data = fd.read(fd.stat.size)
        @common = data.split(/\n/).compact.uniq
        @common.delete("")
      end
    end
    @common
  end

  def parse_users(buff)
    buff.each_line do |line|
      uid = nil
      next if line.strip.empty?

      # Ignore Cisco systems
      return if line =~ /Line.*User.*Host.*Location/

      next if line =~ /user not found/i
      next if line =~ /no such user/i
      next if line =~ /must provide username/
      next if line =~ /real life: \?\?\?/
      next if line =~ /No one logged on/
      next if line =~ /^Login\s+Name\s+TTY/

      # print_status(">> #{line}")

      # No such file or directory == valid user bad utmp


      case line
      when /^([a-z0-9\.\_]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)/
        # Solaris
        uid = $1
        if ($2 != "Name")
          @users[uid] ||= {}
        end

      when /^\s*Login name:\s*([^\s]+)\s+/i
        # IRIX
        uid = $1
        @users[uid] ||= {} if uid
      when /^\s*(?:Username|Login):\s*([^\s]+)\s+/i
        # Debian GNU/Linux
        uid = $1
        @users[uid] ||= {} if uid
      end

      if uid
        print_good "#{rhost}:#{rport} - Found user: #{uid}" unless @users[uid] == :reported
        @users[uid] = :reported
        next
      end
    end
    return true
  end
end
