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

require 'uri'

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

  URLS = [
      '/stmeetings/about.jsp',
      '/stmeetings/serverversion.properties',
      '/rtc/buildinfo.txt',
      '/stmeetings/configuration?format=json&verbose=true'
  ]

  PROXY_URLS = [
      '/stwebclient/i18nStrings.jsp',
      '/stwebclient/communityserver',
      '/stwebav/WebAVServlet?Name=WebPlayerVersion'
  ]

  JSON_KEYS = [
    'communityRef',
    'anonymousEnabled',
    'calinteg.enabled',
    'docshare.fileio.codebase',
    'docshare.native.codebase',
    'docshare.remote.url',
    'meetingroom.allowGuestAccess',
    'meetingroomcenter.allowGuestAccess',
    'meetingroomcenter.customLoginPage',
    'meetingroomcenter.enforceCSRFToken',
    'meetingroomcenter.enforceHiddenRooms',
    'meetingroomcenter.passwords',
    'meetingserver.statistics.jmx.enabled',
    'rtc4web.enforceNonce',
    'userInfoRedirect',
    'userInfoUrlTemplate',
    'meetingroomcenter.stProxyAddress',
    'meetingroomcenter.stProxySSLAddress'
  ]

  INFO_REGEXS = [
    # section, key, regex
    [ 'version', 'sametimeVersion', /lotusBuild">Release (.+?)<\/td>/i ],
    [ 'api', 'meeting',  /^meeting=(.*)$/i ],
    [ 'api', 'appshare', /^appshare=(.*)$/i ],
    [ 'api', 'docshare', /^docshare=(.*)$/i ],
    [ 'api', 'rtc4web', /^rtc4web=(.*)$/i ],
    [ 'api', 'roomapi', /^roomapi=(.*)$/i ],
    [ 'api', 'recordings', /^recordings=(.*)$/i ],
    [ 'api', 'audio', /^audio=(.*)$/i ],
    [ 'api', 'video', /^video=(.*)$/i]
  ]


  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'IBM Lotus Sametime Version Enumeration',
      'Description' => %q{
        This module scans an IBM Lotus Sametime web interface to enumerate
        the application's version and configuration information.
      },
      'Author'         =>
        [
          'kicks4kittens' # Metasploit module
        ],
      'References' =>
        [
          [ 'CVE', '2013-3982' ],
          [ '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('TARGETURI', [ true,  "The path to the Sametime Server", '/']),
        OptBool.new('QuerySametimeProxy', [ true,  "Automatically query Sametime proxy if found", true]),
        OptBool.new('ShowVersions', [ true,  "Display Version information from server", true]),
        OptBool.new('ShowConfig', [ true,  "Display Config information from server", true]),
        OptBool.new('ShowAPIVersions', [ true,  "Display API Version information from server", false])
      ])

    register_advanced_options(
      [
        OptBool.new('StoreConfigs', [ true,  "Store JSON configs to loot", true])
      ])

  end

  def check_url(url, proxy='')

    cgi_options = {
      'uri' => normalize_uri(target_path, url),
      'method' => 'GET'
    }

    if proxy.empty?
      checked_host = datastore['RHOST']
    else
      checked_host = proxy
      cgi_options.merge!({
        'rhost' => proxy, # connect to Sametime Proxy
        'vhost' => proxy  # set appropriate VHOST
      })
    end

    vprint_status("Requesting \"#{checked_host}:#{rport}#{normalize_uri(target_uri.path, url)}\"")
    res = send_request_cgi(cgi_options)

    if res.nil?
      print_status("#{checked_host}:#{rport} - Did not respond")
      return
    elsif res.code == 403
      print_status("#{checked_host}:#{rport} - Access Denied #{res.code} #{res.message}")
      return
    elsif res.code != 200
      print_error("#{checked_host}:#{rport} - Unexpected Response code (#{res.code}) received from server")
      return
    end

    if url.include?('WebAVServlet')
      # special handler for WebAVServlet as body is JSON regardless of content-type
      begin
        res_json = JSON.parse(res.body)
      rescue JSON::ParserError
        print_error("#{checked_host}:#{rport} - Unable to parse JSON response")
      end
      extract_webavservlet_data(res_json)
    elsif res['content-type'].include?("text/plain") or res['content-type'].include?("text/html")
      extract_data(res.body, url)
    elsif res['content-type'].include?("text/json") or res['content-type'].include?("text/javaScript")
      begin
        res_json = JSON.parse(res.body)
      rescue JSON::ParserError
        print_error("#{checked_host}:#{rport} - Unable to parse JSON response")
      end
      # store configuration files as loot
      store_config(url, res_json, checked_host) if datastore['StoreConfigs']
      extract_json_data(res_json)
    end
  end

  # extract data from WebAVServlet
  def extract_webavservlet_data(res_json)
    # stwebav/WebAVServlet --> WebPlayer information
    if res_json['Softphone']
      @version_info['version']['Softphone'] = res_json['Softphone']
    end

    if res_json['WebPlayer']
      @version_info['version']['WebPlayer'] = res_json['WebPlayer']
    end
  end

  def extract_data(data, url)
    # extract data from response
    INFO_REGEXS.each do |regex|
      if data =~ regex[2]
        @version_info[regex[0]][regex[1]] = $1.chomp
      end
    end

    if url.include?('buildinfo.txt') and data =~ /^(\d{8}-\d+)$/
      @version_info['version']['buildinfo'] = $1.chomp
    end

    if data =~ /aboutBoxProductTitle":"(.*?)",/i
      @version_info['version']['sametimeVersion'] = $1.chomp unless @version_info['version']['sametimeVersion']
    end
  end

  # extract data from JSON response
  def extract_json_data(json)
    JSON_KEYS.each do |k|
      @version_info['conf'][k] = json[k] if json[k]
    end
  end

  def report
    if @version_info['version']['sametimeVersion']
      print_line
      print_good("#{@version_info['version']['sametimeVersion']} Detected")
    else
      print_line
      print_status("IBM Lotus Sametime information")
    end

    # configure tables
    version_tbl = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header'  => "IBM Lotus Sametime Information [Version]",
      'Prefix'  => "",
      'Indent'  => 1,
      'Columns'   =>
      [
        "Component",
        "Version"
      ])

    conf_tbl = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header'  => "IBM Lotus Sametime Information [Config]",
      'Prefix'  => "",
      'Indent'  => 1,
      'Columns'   =>
      [
        "Key",
        "Value"
      ])

    api_tbl = Msf::Ui::Console::Table.new(
      Msf::Ui::Console::Table::Style::Default,
      'Header'  => "IBM Lotus Sametime Information [API]",
      'Prefix'  => "",
      'Indent'  => 1,
      'Columns'   =>
      [
        "API",
        "Version"
      ])

    # populate tables
    @version_info['version'].each do | line |
      version_tbl << [ line[0], line[1] ]
    end

    @version_info['conf'].each do | line |
      conf_tbl << [ line[0], line[1] ]
    end

    @version_info['api'].each do | line |
      api_tbl << [ line[0], line[1] ]
    end

    # display tables
    print_good("#{version_tbl.to_s}") if not version_tbl.to_s.empty? and datastore['ShowVersions']
    print_good("#{api_tbl.to_s}") if not api_tbl.to_s.empty? and datastore['ShowAPIVersions']
    print_good("#{conf_tbl.to_s}") if not conf_tbl.to_s.empty? and datastore['ShowConfig']

    # report_note
    report_note(
      :host  => rhost,
      :port  => rport,
      :proto => 'http',
      :ntype => 'ibm_lotus_sametime_version',
      :data  => @version_info['version']['sametimeVersion']
    ) if @version_info['version']['sametimeVersion']
  end

  def store_config(url, config_to_store, checked_host)
    # store configuration as loot
    unless config_to_store.empty?
      loot = store_loot(
        "ibm_lotus_sametime_configuration_" + url,
        "text/json",
        datastore['rhost'],
        config_to_store,
        ".json"
      )
      print_good("#{checked_host} - IBM Lotus Sametime Configuration data stored as loot")
      print_status("#{checked_host}#{normalize_uri(target_uri.path, url)}\n => #{loot}")
    end
  end

  def target_path
    normalize_uri(target_uri.path)
  end

  def proxy?
    @version_info['conf']['meetingroomcenter.stProxyAddress'] or @version_info['conf']['meetingroomcenter.stProxySSLAddress']
  end

  def use_proxy?
    datastore['QuerySametimeProxy']
  end

  def proxy_ssl?
    @version_info['conf']['meetingroomcenter.stProxySSLAddress']
  end

  def run
    # create storage for extracted information+
    @version_info = {}
    @version_info['version'] = {}
    @version_info['conf'] = {}
    @version_info['api'] = {}

    print_status("Checking IBM Lotus Sametime Server")
    URLS.each do | url |
      check_url(url)
    end

    if proxy? and use_proxy?
      # check Sametime proxy if configured to do so
      if proxy_ssl? and ssl
        # keep using SSL
        proxy = URI(@version_info['conf']['meetingroomcenter.stProxySSLAddress']).host
      else
        proxy = URI(@version_info['conf']['meetingroomcenter.stProxyAddress']).host
      end

      print_good("Sametime Proxy address discovered #{proxy}")

      PROXY_URLS.each do | url |
        check_url(url, proxy)
      end
    elsif proxy?
      print_status("Sametime Proxy address discovered, but checks disabled")
    end

    report unless @version_info.empty?
  end
end
