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

require 'rex/proto/http'

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


  def initialize(info = {})
    super(update_info(info,
      'Name'   		=> 'HTTP Error Based SQL Injection Scanner',
      'Description'	=> %q{
        This module identifies the existence of Error Based SQL injection issues. Still requires a lot of work

      },
      'Author' 		=> [ 'et [at] cyberspace.org' ],
      'License'		=> BSD_LICENSE))

    register_options(
      [
        OptEnum.new('METHOD', [true, 'HTTP Request Method', 'GET', ['GET', 'POST']]),
        OptString.new('PATH', [ true,  "The path/file to test SQL injection", '/default.aspx']),
        OptString.new('QUERY',[ false,  "HTTP URI Query", '']),
        OptString.new('DATA', [ false,  "HTTP Body/Data Query", ''])
      ])

    register_advanced_options(
      [
        OptBool.new('NoDetailMessages', [ false, "Do not display detailed test messages", true ])
      ])

  end

  def run_host(ip)

    http_method = datastore['METHOD'].upcase

    qvars = nil

    sqlinj = [
      [ "'" ,'Single quote'],
      [ "')",'Single quote and parenthesis'],
      [ "\"",'Double quote'],
      [ "%u0027",'unicode single quote'],
      [ "%u02b9",'unicode single quote'],
      [ "%u02bc",'unicode single quote'],
      [ "%u02c8",'unicode single quote'],
      [ "%c0%27",'unicode single quote'],
      [ "%c0%a7",'unicode single quote'],
      [ "%e0%80%a7",'unicode single quote'],
      [ "#{rand(10)}'", 'Random value with single quote']
    ]

    errorstr = [
      ["Unclosed quotation mark after the character string",'MSSQL','string'],
      ["Syntax error in string in query expression",'MSSQL','string'],
      ["Microsoft OLE DB Provider",'MSSQL','unknown'],
      ["You have an error in your SQL syntax",'MySQL','unknown'],
      ["java.sql.SQLException",'unknown','unknown'],
      ["ORA-",'ORACLE','unknown'],
      ["PLS-",'ORACLE','unknown'],
      ["Syntax error",'unknown','unknown'],
    ]

    #
    # Dealing with empty query/data and making them hashes.
    #

    if  http_method =='GET'
      if not datastore['QUERY'].empty?
        qvars = queryparse(datastore['QUERY']) #Now its a Hash
      else
        print_error("You need to set QUERY param for GET")
        return
      end
    else
      if not datastore['DATA'].empty?
        qvars = queryparse(datastore['DATA']) #Now its a Hash
      else
        print_error("You need to set DATA parameter for POST")
        return
      end
    end

    #
    # Send normal request to check if error is generated
    # (means the error is caused by other means)
    #
    #

    if http_method == 'POST'
      reqinfo = {
        'uri'  		=> normalize_uri(datastore['PATH']),
        'query' 	=> datastore['QUERY'],
        'data' 		=> datastore['DATA'],
        'method'   	=> http_method,
        'ctype'		=> 'application/x-www-form-urlencoded',
        'encode'	=> false
      }
    else
      reqinfo = {
        'uri'  		=> normalize_uri(datastore['PATH']),
        'query' 	=> datastore['QUERY'],
        'method'   	=> http_method,
        'ctype'		=> 'application/x-www-form-urlencoded',
        'encode'	=> false
      }
    end

    begin
      normalres = send_request_raw(reqinfo, 20)

    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
    rescue ::Timeout::Error, ::Errno::EPIPE
    end

    if !datastore['NoDetailMessages']
      print_status("Normal request sent.")
    end

    found = false
    inje = nil
    dbt = nil
    injt = nil

    if normalres
      errorstr.each do |estr,dbtype,injtype|
        if normalres.body.include? estr
          found = true
          inje = estr
          dbt = dbtype
          injt = injtype
        end
      end

      if found
        print_error("[#{wmap_target_host}] Error string appears in the normal response, unable to test")
        print_error("[#{wmap_target_host}] Error string: '#{inje}'")
        print_error("[#{wmap_target_host}] DB TYPE: #{dbt}, Error type '#{injt}'")

        report_web_vuln(
          :host	=> ip,
          :port	=> rport,
          :vhost  => vhost,
          :ssl    => ssl,
          :path	=> datastore['PATH'],
          :method => datastore['METHOD'],
          :pname  => "",
          :proof  => "Error: #{inje}",
          :risk   => 2,
          :confidence   => 50,
          :category     => 'Database error',
          :description  => "Error string appears in the normal response #{inje} #{dbt}",
          :name   => 'Database error'
        )

        return
      end
    else
      print_error("[#{wmap_target_host}] No response")
      return
    end

    #
    # Test URI Query parameters
    #

    found = false

    if qvars
      sqlinj.each do |istr,idesc|

        if found
          break
        end

        qvars.each do |key,value|
          if http_method == 'POST'
            qvars = queryparse(datastore['DATA']) #Now its a Hash
          else
            qvars = queryparse(datastore['QUERY']) #Now its a Hash
          end
          qvars[key] = qvars[key]+istr

          if !datastore['NoDetailMessages']
            print_status("- Testing query with #{idesc}. Parameter #{key}:")
          end

          fstr = ""
          qvars.each_pair do |var,val|
            fstr += var+"="+val+"&"
          end

          if http_method == 'POST'
            reqinfo = {
              'uri'  		=> normalize_uri(datastore['PATH']),
              'query'		=> datastore['QUERY'],
              'data' 		=> fstr,
              'method'   	=> http_method,
              'ctype'		=> 'application/x-www-form-urlencoded',
              'encode'	=> false
            }
          else
            reqinfo = {
              'uri'  		=> normalize_uri(datastore['PATH']),
              'query' 	=> fstr,
              'method'   	=> http_method,
              'ctype'		=> 'application/x-www-form-urlencoded',
              'encode'	=> false
            }
          end

          begin

            testres = send_request_raw(reqinfo, 20)

          rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
          rescue ::Timeout::Error, ::Errno::EPIPE
          end

          if testres
            errorstr.each do |estr,dbtype,injtype|
              if testres.body.include? estr
                found = true
                inje = estr
                dbt = dbtype
                injt = injtype
              end
            end

            if found
              print_good("[#{wmap_target_host}] SQL Injection found. (#{idesc}) (#{datastore['PATH']})")
              print_good("[#{wmap_target_host}] Error string: '#{inje}' Test Value: #{qvars[key]}")
              print_good("[#{wmap_target_host}] Vuln query parameter: #{key} DB TYPE: #{dbt}, Error type '#{injt}'")

              report_web_vuln(
                :host	=> ip,
                :port	=> rport,
                :vhost  => vhost,
                :ssl    => ssl,
                :path	=> datastore['PATH'],
                :method => datastore['METHOD'],
                :pname  => key,
                :proof  => istr,
                :risk   => 2,
                :confidence   => 50,
                :category     => 'SQL injection',
                :description  => "Error string appears in the normal response #{inje} #{dbt}",
                :name   => 'SQL injection'
              )

              return
            end
          else
            print_error("[#{wmap_target_host}] No response")
            return
          end
        end
      end

      if http_method == 'POST'
        qvars = queryparse(datastore['DATA']) #Now its a Hash
      else
        qvars = queryparse(datastore['QUERY']) #Now its a Hash
      end
    end
  end
end
